create-windy 0.3.2 → 0.3.5
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 +27 -9
- package/dist/cli.js +825 -117
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/_gitignore +1 -0
- package/template/apps/agent-server/README.md +2 -0
- package/template/apps/agent-server/pyproject.toml +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
- package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
- package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
- package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
- package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
- package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
- package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
- package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
- package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
- package/template/apps/agent-server/tests/test_api.py +24 -1
- package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
- package/template/apps/agent-server/tests/test_config.py +4 -0
- package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
- package/template/apps/agent-server/tests/test_multimodal.py +51 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
- package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
- package/template/apps/agent-server/tests/test_telemetry.py +25 -0
- package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
- package/template/apps/agent-server/uv.lock +165 -0
- package/template/apps/document-server/Dockerfile +19 -0
- package/template/apps/document-server/pyproject.toml +43 -0
- package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
- package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
- package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
- package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
- package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
- package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
- package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
- package/template/apps/document-server/tests/__init__.py +1 -0
- package/template/apps/document-server/tests/test_config.py +42 -0
- package/template/apps/document-server/tests/test_service.py +140 -0
- package/template/apps/document-server/uv.lock +430 -0
- package/template/apps/embedding-server/Dockerfile +17 -0
- package/template/apps/embedding-server/pyproject.toml +44 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
- package/template/apps/embedding-server/tests/__init__.py +1 -0
- package/template/apps/embedding-server/tests/test_adapter.py +92 -0
- package/template/apps/embedding-server/tests/test_config.py +38 -0
- package/template/apps/embedding-server/uv.lock +449 -0
- package/template/apps/server/src/agent/background-operation-port.ts +37 -0
- package/template/apps/server/src/agent/background-operation.test.ts +231 -0
- package/template/apps/server/src/agent/background-operation.ts +252 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
- package/template/apps/server/src/agent/execution-grants.ts +7 -1
- package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
- package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
- package/template/apps/server/src/agent/idempotency-key.ts +11 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
- package/template/apps/server/src/agent/remote-runtime.ts +173 -5
- package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
- package/template/apps/server/src/agent/run-authorization.ts +180 -0
- package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
- package/template/apps/server/src/agent/run-contracts.ts +88 -1
- package/template/apps/server/src/agent/run-definition-support.ts +58 -0
- package/template/apps/server/src/agent/run-errors.ts +25 -0
- package/template/apps/server/src/agent/run-facade.test.ts +57 -46
- package/template/apps/server/src/agent/run-facade.ts +185 -65
- package/template/apps/server/src/agent/run-release.ts +78 -0
- package/template/apps/server/src/agent/run-route-input.ts +169 -0
- package/template/apps/server/src/agent/run-routes.ts +63 -76
- package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
- package/template/apps/server/src/agent/sandbox-client.ts +200 -0
- package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
- package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
- package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
- package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
- package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
- package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
- package/template/apps/server/src/agent/tool-approval.ts +115 -0
- package/template/apps/server/src/agent/tool-audit.ts +62 -0
- package/template/apps/server/src/agent/tool-host.test.ts +39 -11
- package/template/apps/server/src/agent/tool-host.ts +184 -17
- package/template/apps/server/src/agent/tool-registry.ts +52 -47
- package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
- package/template/apps/server/src/agent/vision-input.ts +120 -0
- package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
- package/template/apps/server/src/ai-artifact/codec.ts +137 -0
- package/template/apps/server/src/ai-artifact/routes.ts +37 -0
- package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
- package/template/apps/server/src/application-services.ts +62 -2
- package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
- package/template/apps/server/src/auth/reauthentication.ts +19 -0
- package/template/apps/server/src/auth/runtime.ts +9 -1
- package/template/apps/server/src/data-access/context.ts +6 -4
- package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
- package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
- package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
- package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
- package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
- package/template/apps/server/src/data-governance/export/service.ts +90 -113
- package/template/apps/server/src/data-governance/export/source.ts +101 -0
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/document/contracts.ts +76 -0
- package/template/apps/server/src/document/facade.ts +339 -0
- package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
- package/template/apps/server/src/document/remote-runtime.ts +223 -0
- package/template/apps/server/src/document/routes.ts +193 -0
- package/template/apps/server/src/embedding/contracts.ts +83 -0
- package/template/apps/server/src/embedding/error.ts +10 -0
- package/template/apps/server/src/embedding/facade.ts +217 -0
- package/template/apps/server/src/embedding/input.ts +172 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
- package/template/apps/server/src/embedding/routes.ts +152 -0
- package/template/apps/server/src/index.ts +40 -43
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/execution.test.ts +34 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
- package/template/apps/server/src/jobs/repository.ts +3 -0
- package/template/apps/server/src/jobs/worker.ts +80 -41
- package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
- package/template/apps/server/src/module-host/durable-context.ts +124 -0
- package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
- package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
- package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
- package/template/apps/server/src/module-host/governed-resources.ts +203 -0
- package/template/apps/server/src/module-host/http-runtime.ts +95 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +49 -1
- package/template/apps/server/src/module-host/initialize.ts +39 -16
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.test.ts +88 -2
- package/template/apps/server/src/module-host/request-context.ts +56 -22
- package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
- package/template/apps/server/src/module-host/route-installer.ts +36 -1
- package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/module-runtime-validation.ts +7 -3
- package/template/apps/server/src/speech/contracts.ts +76 -0
- package/template/apps/server/src/speech/facade.ts +316 -0
- package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
- package/template/apps/server/src/speech/remote-runtime.ts +227 -0
- package/template/apps/server/src/speech/routes.ts +180 -0
- package/template/apps/server/src/system/actor.ts +39 -0
- package/template/apps/server/src/system/route-helpers.ts +1 -0
- package/template/apps/server/src/workflow/service.ts +2 -1
- package/template/apps/speech-server/Dockerfile +22 -0
- package/template/apps/speech-server/pyproject.toml +43 -0
- package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
- package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
- package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
- package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
- package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
- package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
- package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
- package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
- package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
- package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
- package/template/apps/speech-server/tests/__init__.py +1 -0
- package/template/apps/speech-server/tests/test_config.py +33 -0
- package/template/apps/speech-server/tests/test_service.py +154 -0
- package/template/apps/speech-server/uv.lock +430 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/components/common/FormDialog.vue +6 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docker-compose.yml +127 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/ai-runtime.md +23 -11
- package/template/docs/architecture/durable-jobs.md +25 -3
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +21 -11
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/agent-tools.md +5 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/package.json +16 -2
- package/template/packages/ai-client/index.ts +4 -0
- package/template/packages/ai-client/package.json +5 -2
- package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
- package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
- package/template/packages/ai-client/src/agent/client/client.ts +119 -7
- package/template/packages/ai-client/src/agent/client/run.ts +14 -0
- package/template/packages/ai-client/src/agent/client/types.ts +18 -0
- package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
- package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
- package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
- package/template/packages/ai-client/src/agent/index.ts +1 -0
- package/template/packages/ai-client/src/artifact-ref.ts +8 -0
- package/template/packages/ai-client/src/document/client.test.ts +114 -0
- package/template/packages/ai-client/src/document/client.ts +134 -0
- package/template/packages/ai-client/src/document/contracts.ts +152 -0
- package/template/packages/ai-client/src/document/error.ts +43 -0
- package/template/packages/ai-client/src/document/index.ts +3 -0
- package/template/packages/ai-client/src/document/run.ts +104 -0
- package/template/packages/ai-client/src/document/validation.ts +115 -0
- package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
- package/template/packages/ai-client/src/embedding/client.ts +96 -0
- package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
- package/template/packages/ai-client/src/embedding/error.ts +61 -0
- package/template/packages/ai-client/src/embedding/index.ts +3 -0
- package/template/packages/ai-client/src/embedding/validation.ts +117 -0
- package/template/packages/ai-client/src/speech/client.test.ts +135 -0
- package/template/packages/ai-client/src/speech/client.ts +104 -0
- package/template/packages/ai-client/src/speech/contracts.ts +129 -0
- package/template/packages/ai-client/src/speech/error.ts +43 -0
- package/template/packages/ai-client/src/speech/index.ts +3 -0
- package/template/packages/ai-client/src/speech/run.ts +133 -0
- package/template/packages/ai-client/src/speech/validation.ts +132 -0
- package/template/packages/config/package.json +2 -2
- package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
- package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
- package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
- package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
- package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +149 -0
- package/template/packages/database/src/schema/common.ts +2 -14
- package/template/packages/database/src/schema/durable-jobs.ts +5 -0
- package/template/packages/database/src/schema/governed-exports.ts +17 -5
- package/template/packages/database/src/schema/index.ts +3 -0
- package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/database/src/schema.test.ts +6 -23
- package/template/packages/jobs/package.json +3 -2
- package/template/packages/jobs/src/handlers.test.ts +13 -0
- package/template/packages/jobs/src/handlers.ts +7 -1
- package/template/packages/jobs/src/types.ts +4 -0
- package/template/packages/modules/package.json +2 -2
- package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
- package/template/packages/modules/src/ai-operation-composition.ts +59 -1
- package/template/packages/modules/src/manifest.ts +25 -1
- package/template/packages/modules/src/tool-composition.ts +18 -0
- package/template/packages/server-sdk/index.ts +50 -0
- package/template/packages/server-sdk/package.json +10 -4
- package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
- package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
- package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
- package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
- package/template/packages/server-sdk/src/durable-handler.ts +29 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/module-host.ts +16 -2
- package/template/packages/server-sdk/src/request-context.ts +6 -2
- package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
- package/template/packages/server-sdk/src/tool-registration.ts +2 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/rbac.ts +30 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .config import DocumentRuntimeConfig
|
|
12
|
+
from .contracts import ArtifactInput, JsonValue, StartDocumentRunRequest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SafeDocumentError(Exception):
|
|
16
|
+
def __init__(self, code: str, message: str, retryable: bool) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.code = code
|
|
19
|
+
self.safe_message = message
|
|
20
|
+
self.retryable = retryable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DocumentAdapter:
|
|
24
|
+
def __init__(self, config: DocumentRuntimeConfig) -> None:
|
|
25
|
+
self.config = config
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def available(self) -> bool:
|
|
29
|
+
return self.config.adapter_url is not None
|
|
30
|
+
|
|
31
|
+
async def execute(self, request: StartDocumentRunRequest) -> JsonValue:
|
|
32
|
+
if not self.config.adapter_url:
|
|
33
|
+
raise SafeDocumentError(
|
|
34
|
+
"CAPABILITY_UNAVAILABLE",
|
|
35
|
+
"Document Adapter 未配置",
|
|
36
|
+
True,
|
|
37
|
+
)
|
|
38
|
+
missing = set(request.required_capabilities) - set(self.config.capabilities)
|
|
39
|
+
if missing:
|
|
40
|
+
raise SafeDocumentError(
|
|
41
|
+
"CAPABILITY_UNAVAILABLE",
|
|
42
|
+
"Document Adapter 缺少所需能力",
|
|
43
|
+
True,
|
|
44
|
+
)
|
|
45
|
+
self._authorize_deployment(request)
|
|
46
|
+
if request.limits.max_units > self.config.max_units:
|
|
47
|
+
raise SafeDocumentError(
|
|
48
|
+
"BUDGET_EXCEEDED",
|
|
49
|
+
"Document 页数上限超过 Runtime 配置",
|
|
50
|
+
False,
|
|
51
|
+
)
|
|
52
|
+
paths: list[Path] = []
|
|
53
|
+
try:
|
|
54
|
+
for source in request.sources:
|
|
55
|
+
paths.append(await self._download(request, source))
|
|
56
|
+
return await self._invoke(request, paths)
|
|
57
|
+
finally:
|
|
58
|
+
for path in paths:
|
|
59
|
+
await remove(path)
|
|
60
|
+
|
|
61
|
+
async def _download(
|
|
62
|
+
self, request: StartDocumentRunRequest, source: ArtifactInput
|
|
63
|
+
) -> Path:
|
|
64
|
+
digest = hashlib.sha256()
|
|
65
|
+
total = 0
|
|
66
|
+
with tempfile.NamedTemporaryFile(delete=False) as target:
|
|
67
|
+
path = Path(target.name)
|
|
68
|
+
try:
|
|
69
|
+
async with httpx.AsyncClient(
|
|
70
|
+
timeout=httpx.Timeout(
|
|
71
|
+
self.config.request_timeout_seconds,
|
|
72
|
+
connect=self.config.connect_timeout_seconds,
|
|
73
|
+
)
|
|
74
|
+
) as client:
|
|
75
|
+
async with client.stream(
|
|
76
|
+
"GET",
|
|
77
|
+
self.config.artifact_url,
|
|
78
|
+
headers={
|
|
79
|
+
"authorization": f"Bearer {self.config.internal_token}",
|
|
80
|
+
"x-ai-artifact-ref": source.value,
|
|
81
|
+
},
|
|
82
|
+
) as response:
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
async for chunk in response.aiter_bytes():
|
|
85
|
+
total += len(chunk)
|
|
86
|
+
if total > min(
|
|
87
|
+
source.byte_size,
|
|
88
|
+
request.limits.max_input_bytes,
|
|
89
|
+
):
|
|
90
|
+
raise SafeDocumentError(
|
|
91
|
+
"BUDGET_EXCEEDED",
|
|
92
|
+
"文档输入超过声明上限",
|
|
93
|
+
False,
|
|
94
|
+
)
|
|
95
|
+
digest.update(chunk)
|
|
96
|
+
target.write(chunk)
|
|
97
|
+
except SafeDocumentError:
|
|
98
|
+
await remove(path)
|
|
99
|
+
raise
|
|
100
|
+
except Exception as error:
|
|
101
|
+
await remove(path)
|
|
102
|
+
raise SafeDocumentError(
|
|
103
|
+
"ARTIFACT_UNAVAILABLE",
|
|
104
|
+
"Document Runtime 无法读取 Artifact",
|
|
105
|
+
True,
|
|
106
|
+
) from error
|
|
107
|
+
if total != source.byte_size or digest.hexdigest() != source.sha256:
|
|
108
|
+
await remove(path)
|
|
109
|
+
raise SafeDocumentError(
|
|
110
|
+
"ARTIFACT_UNAVAILABLE",
|
|
111
|
+
"Artifact 完整性校验失败",
|
|
112
|
+
False,
|
|
113
|
+
)
|
|
114
|
+
return path
|
|
115
|
+
|
|
116
|
+
async def _invoke(
|
|
117
|
+
self, request: StartDocumentRunRequest, paths: list[Path]
|
|
118
|
+
) -> JsonValue:
|
|
119
|
+
adapter_url = self.config.adapter_url
|
|
120
|
+
if not adapter_url:
|
|
121
|
+
raise SafeDocumentError(
|
|
122
|
+
"CAPABILITY_UNAVAILABLE", "Document Adapter 未配置", True
|
|
123
|
+
)
|
|
124
|
+
headers = (
|
|
125
|
+
{"authorization": f"Bearer {self.config.adapter_token}"}
|
|
126
|
+
if self.config.adapter_token
|
|
127
|
+
else {}
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
async with httpx.AsyncClient(
|
|
131
|
+
timeout=httpx.Timeout(
|
|
132
|
+
self.config.request_timeout_seconds,
|
|
133
|
+
connect=self.config.connect_timeout_seconds,
|
|
134
|
+
)
|
|
135
|
+
) as client:
|
|
136
|
+
opened = [path.open("rb") for path in paths]
|
|
137
|
+
try:
|
|
138
|
+
response = await client.post(
|
|
139
|
+
f"{adapter_url.rstrip('/')}/v1/document/process",
|
|
140
|
+
headers=headers,
|
|
141
|
+
files=[
|
|
142
|
+
(
|
|
143
|
+
"file" if len(opened) == 1 else "files",
|
|
144
|
+
(
|
|
145
|
+
f"source-{index + 1}",
|
|
146
|
+
source,
|
|
147
|
+
request.sources[index].media_type,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
for index, source in enumerate(opened)
|
|
151
|
+
],
|
|
152
|
+
data={
|
|
153
|
+
"operationKey": request.operation_key,
|
|
154
|
+
"dataClassification": request.data_classification,
|
|
155
|
+
"options": json.dumps(
|
|
156
|
+
request.options,
|
|
157
|
+
ensure_ascii=False,
|
|
158
|
+
separators=(",", ":"),
|
|
159
|
+
),
|
|
160
|
+
"providerEgress": request.provider_egress.model_dump_json(
|
|
161
|
+
by_alias=True
|
|
162
|
+
),
|
|
163
|
+
"providerDataPolicy": (
|
|
164
|
+
request.provider_data_policy.model_dump_json(
|
|
165
|
+
by_alias=True
|
|
166
|
+
)
|
|
167
|
+
if request.provider_data_policy
|
|
168
|
+
else "null"
|
|
169
|
+
),
|
|
170
|
+
"limits": request.limits.model_dump_json(by_alias=True),
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
finally:
|
|
174
|
+
for source in opened:
|
|
175
|
+
source.close()
|
|
176
|
+
response.raise_for_status()
|
|
177
|
+
if len(response.content) > request.limits.max_output_bytes:
|
|
178
|
+
raise SafeDocumentError(
|
|
179
|
+
"BUDGET_EXCEEDED",
|
|
180
|
+
"Document 输出超过上限",
|
|
181
|
+
False,
|
|
182
|
+
)
|
|
183
|
+
value: JsonValue = response.json()
|
|
184
|
+
validate_output(value, request.limits.max_units)
|
|
185
|
+
return value
|
|
186
|
+
except SafeDocumentError:
|
|
187
|
+
raise
|
|
188
|
+
except Exception as error:
|
|
189
|
+
raise SafeDocumentError(
|
|
190
|
+
"RUNTIME_UNAVAILABLE",
|
|
191
|
+
"Document Adapter 执行失败",
|
|
192
|
+
True,
|
|
193
|
+
) from error
|
|
194
|
+
|
|
195
|
+
def _authorize_deployment(self, request: StartDocumentRunRequest) -> None:
|
|
196
|
+
if request.provider_egress.mode == "forbidden":
|
|
197
|
+
return
|
|
198
|
+
if not request.provider_data_policy:
|
|
199
|
+
raise SafeDocumentError(
|
|
200
|
+
"FORBIDDEN",
|
|
201
|
+
"Document Operation 缺少 Provider 字段治理策略",
|
|
202
|
+
False,
|
|
203
|
+
)
|
|
204
|
+
if (
|
|
205
|
+
not self.config.deployment_key
|
|
206
|
+
or request.provider_egress.deployment_key != self.config.deployment_key
|
|
207
|
+
):
|
|
208
|
+
raise SafeDocumentError(
|
|
209
|
+
"FORBIDDEN",
|
|
210
|
+
"Document Operation 未授权当前 Deployment",
|
|
211
|
+
False,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def validate_output(value: JsonValue, max_units: int) -> None:
|
|
216
|
+
if not isinstance(value, dict):
|
|
217
|
+
raise SafeDocumentError(
|
|
218
|
+
"OUTPUT_VALIDATION_FAILED", "Document 输出结构无效", False
|
|
219
|
+
)
|
|
220
|
+
pages = value.get("pages")
|
|
221
|
+
if not isinstance(pages, list):
|
|
222
|
+
raise SafeDocumentError(
|
|
223
|
+
"OUTPUT_VALIDATION_FAILED", "Document 输出缺少页级结果", False
|
|
224
|
+
)
|
|
225
|
+
if len(pages) > max_units:
|
|
226
|
+
raise invalid_output("Document 输出页数超过上限")
|
|
227
|
+
page_numbers: set[int] = set()
|
|
228
|
+
for page in pages:
|
|
229
|
+
if not isinstance(page, dict):
|
|
230
|
+
raise invalid_output("Document 页级结果无效")
|
|
231
|
+
page_number = page.get("pageNumber")
|
|
232
|
+
if (
|
|
233
|
+
not isinstance(page_number, int)
|
|
234
|
+
or isinstance(page_number, bool)
|
|
235
|
+
or page_number < 1
|
|
236
|
+
or page_number > max_units
|
|
237
|
+
or page_number in page_numbers
|
|
238
|
+
or not isinstance(page.get("text"), str)
|
|
239
|
+
or not isinstance(page.get("blocks"), list)
|
|
240
|
+
):
|
|
241
|
+
raise invalid_output("Document 页码、文本或 Block 无效")
|
|
242
|
+
page_numbers.add(page_number)
|
|
243
|
+
blocks = page.get("blocks")
|
|
244
|
+
if not isinstance(blocks, list):
|
|
245
|
+
raise invalid_output("Document Block 无效")
|
|
246
|
+
for block in blocks:
|
|
247
|
+
validate_block(block)
|
|
248
|
+
layout = page.get("layout")
|
|
249
|
+
if layout is not None:
|
|
250
|
+
if not isinstance(layout, list):
|
|
251
|
+
raise invalid_output("Document 版面树无效")
|
|
252
|
+
for node in layout:
|
|
253
|
+
validate_layout(node, 0)
|
|
254
|
+
tables = value.get("tables", [])
|
|
255
|
+
if not isinstance(tables, list):
|
|
256
|
+
raise invalid_output("Document 表格结果无效")
|
|
257
|
+
for table in tables:
|
|
258
|
+
rows = table.get("rows") if isinstance(table, dict) else None
|
|
259
|
+
if (
|
|
260
|
+
not isinstance(table, dict)
|
|
261
|
+
or table.get("pageNumber") not in page_numbers
|
|
262
|
+
or not isinstance(rows, list)
|
|
263
|
+
or any(
|
|
264
|
+
not isinstance(row, list)
|
|
265
|
+
or any(not isinstance(cell, str) for cell in row)
|
|
266
|
+
for row in rows
|
|
267
|
+
)
|
|
268
|
+
):
|
|
269
|
+
raise invalid_output("Document 表格结果无效")
|
|
270
|
+
validate_bounds(table.get("bounds"))
|
|
271
|
+
if value.get("artifacts", []) != []:
|
|
272
|
+
raise invalid_output("Document Adapter 不得直接签发 Artifact")
|
|
273
|
+
metadata = value.get("metadata", {})
|
|
274
|
+
if not isinstance(metadata, dict) or any(
|
|
275
|
+
not isinstance(item, str | int | float | bool) for item in metadata.values()
|
|
276
|
+
):
|
|
277
|
+
raise invalid_output("Document 元数据无效")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def validate_block(value: object) -> None:
|
|
281
|
+
if (
|
|
282
|
+
not isinstance(value, dict)
|
|
283
|
+
or not isinstance(value.get("id"), str)
|
|
284
|
+
or not isinstance(value.get("text"), str)
|
|
285
|
+
):
|
|
286
|
+
raise invalid_output("Document 文本 Block 无效")
|
|
287
|
+
confidence = value.get("confidence")
|
|
288
|
+
if confidence is not None and (
|
|
289
|
+
not isinstance(confidence, int | float)
|
|
290
|
+
or isinstance(confidence, bool)
|
|
291
|
+
or confidence < 0
|
|
292
|
+
or confidence > 1
|
|
293
|
+
):
|
|
294
|
+
raise invalid_output("Document Block 置信度无效")
|
|
295
|
+
validate_bounds(value.get("bounds"))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def validate_layout(value: object, depth: int) -> None:
|
|
299
|
+
if depth > 32 or not isinstance(value, dict):
|
|
300
|
+
raise invalid_output("Document 版面树无效")
|
|
301
|
+
if not isinstance(value.get("id"), str) or not isinstance(value.get("type"), str):
|
|
302
|
+
raise invalid_output("Document 版面节点无效")
|
|
303
|
+
text = value.get("text")
|
|
304
|
+
if text is not None and not isinstance(text, str):
|
|
305
|
+
raise invalid_output("Document 版面节点文本无效")
|
|
306
|
+
validate_bounds(value.get("bounds"))
|
|
307
|
+
children = value.get("children", [])
|
|
308
|
+
if not isinstance(children, list):
|
|
309
|
+
raise invalid_output("Document 版面子节点无效")
|
|
310
|
+
for child in children:
|
|
311
|
+
validate_layout(child, depth + 1)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def validate_bounds(value: object) -> None:
|
|
315
|
+
if value is None:
|
|
316
|
+
return
|
|
317
|
+
if not isinstance(value, dict) or value.get("unit") not in {
|
|
318
|
+
"pixel",
|
|
319
|
+
"normalized",
|
|
320
|
+
}:
|
|
321
|
+
raise invalid_output("Document 坐标无效")
|
|
322
|
+
for field in ("x", "y", "width", "height"):
|
|
323
|
+
coordinate = value.get(field)
|
|
324
|
+
if (
|
|
325
|
+
not isinstance(coordinate, int | float)
|
|
326
|
+
or isinstance(coordinate, bool)
|
|
327
|
+
or coordinate < 0
|
|
328
|
+
):
|
|
329
|
+
raise invalid_output("Document 坐标无效")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def invalid_output(message: str) -> SafeDocumentError:
|
|
333
|
+
return SafeDocumentError("OUTPUT_VALIDATION_FAILED", message, False)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def remove(path: Path) -> None:
|
|
337
|
+
await asyncio.to_thread(path.unlink, missing_ok=True)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
|
|
9
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
10
|
+
from fastapi.exceptions import RequestValidationError
|
|
11
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
12
|
+
|
|
13
|
+
from .adapter import DocumentAdapter, SafeDocumentError
|
|
14
|
+
from .config import DocumentRuntimeConfig
|
|
15
|
+
from .contracts import (
|
|
16
|
+
ErrorResponse,
|
|
17
|
+
StartDocumentRunRequest,
|
|
18
|
+
StartRunResponse,
|
|
19
|
+
)
|
|
20
|
+
from .service import DocumentRunService
|
|
21
|
+
from .store import DocumentRunStore, RunConflictError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_app(config: DocumentRuntimeConfig) -> FastAPI:
|
|
25
|
+
app = FastAPI(title="Southwind Document Runtime", docs_url=None, redoc_url=None)
|
|
26
|
+
store = DocumentRunStore(config.store_path)
|
|
27
|
+
service = DocumentRunService(DocumentAdapter(config), store)
|
|
28
|
+
|
|
29
|
+
def authorize(authorization: str | None = Header(default=None)) -> None:
|
|
30
|
+
provided = (authorization or "").removeprefix("Bearer ")
|
|
31
|
+
if not provided or not hmac.compare_digest(provided, config.internal_token):
|
|
32
|
+
raise HTTPException(status_code=403, detail="FORBIDDEN")
|
|
33
|
+
|
|
34
|
+
@app.get("/health/ready")
|
|
35
|
+
async def ready(_: None = Depends(authorize)) -> JSONResponse:
|
|
36
|
+
if not service.adapter.available:
|
|
37
|
+
return JSONResponse(
|
|
38
|
+
{"status": "unavailable", "capabilities": []},
|
|
39
|
+
status_code=503,
|
|
40
|
+
)
|
|
41
|
+
return JSONResponse(
|
|
42
|
+
{
|
|
43
|
+
"status": "ready",
|
|
44
|
+
"capabilities": list(config.capabilities),
|
|
45
|
+
"limits": {"maxUnits": config.max_units},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@app.post("/api/document/runs", status_code=202)
|
|
50
|
+
async def start(
|
|
51
|
+
request: StartDocumentRunRequest,
|
|
52
|
+
idempotency_key: str = Header(alias="idempotency-key"),
|
|
53
|
+
_: None = Depends(authorize),
|
|
54
|
+
) -> StartRunResponse:
|
|
55
|
+
if not idempotency_key.strip() or len(idempotency_key) > 160:
|
|
56
|
+
raise HTTPException(status_code=400, detail="INVALID_INPUT")
|
|
57
|
+
try:
|
|
58
|
+
record = await service.start(request, idempotency_key)
|
|
59
|
+
except RunConflictError:
|
|
60
|
+
raise HTTPException(status_code=409, detail="RUN_CONFLICT") from None
|
|
61
|
+
return StartRunResponse(
|
|
62
|
+
run_id=record.run_id,
|
|
63
|
+
status=record.status,
|
|
64
|
+
events_url=f"/api/document/runs/{record.run_id}/events",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@app.get("/api/document/runs/{run_id}/events")
|
|
68
|
+
async def events(
|
|
69
|
+
run_id: str,
|
|
70
|
+
after: int = Query(default=0, ge=0),
|
|
71
|
+
_: None = Depends(authorize),
|
|
72
|
+
) -> StreamingResponse:
|
|
73
|
+
service.require(run_id)
|
|
74
|
+
|
|
75
|
+
async def stream() -> AsyncIterator[str]:
|
|
76
|
+
cursor = after
|
|
77
|
+
for _attempt in range(30):
|
|
78
|
+
current = service.require(run_id)
|
|
79
|
+
events = store.events_after(run_id, cursor)
|
|
80
|
+
for event in events:
|
|
81
|
+
sequence = event.get("sequence")
|
|
82
|
+
if not isinstance(sequence, int):
|
|
83
|
+
raise RuntimeError("Document Event sequence 无效")
|
|
84
|
+
cursor = sequence
|
|
85
|
+
yield f"data: {encode_event(event)}\n\n"
|
|
86
|
+
if current.terminal:
|
|
87
|
+
return
|
|
88
|
+
if not events:
|
|
89
|
+
yield ": heartbeat\n\n"
|
|
90
|
+
await asyncio.sleep(1)
|
|
91
|
+
|
|
92
|
+
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
93
|
+
|
|
94
|
+
@app.get("/api/document/runs/{run_id}/result")
|
|
95
|
+
async def result(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
|
|
96
|
+
record = service.require(run_id)
|
|
97
|
+
if record.status != "completed":
|
|
98
|
+
return JSONResponse({"code": "RUN_NOT_READY"}, status_code=409)
|
|
99
|
+
return JSONResponse(
|
|
100
|
+
{"runId": run_id, "status": "completed", "output": record.output}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@app.post("/api/document/runs/{run_id}/cancel")
|
|
104
|
+
async def cancel(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
|
|
105
|
+
record = await service.cancel(run_id)
|
|
106
|
+
return JSONResponse({"runId": run_id, "status": record.status})
|
|
107
|
+
|
|
108
|
+
@app.exception_handler(KeyError)
|
|
109
|
+
async def missing(_request: object, _error: KeyError) -> JSONResponse:
|
|
110
|
+
return safe_error("RUN_NOT_FOUND", "Document Run 不存在", 404, False)
|
|
111
|
+
|
|
112
|
+
@app.exception_handler(HTTPException)
|
|
113
|
+
async def http_error(_request: object, error: HTTPException) -> JSONResponse:
|
|
114
|
+
code = str(error.detail)
|
|
115
|
+
return safe_error(code, "Document 请求被拒绝", error.status_code, False)
|
|
116
|
+
|
|
117
|
+
@app.exception_handler(RequestValidationError)
|
|
118
|
+
async def validation_error(
|
|
119
|
+
_request: object, _error: RequestValidationError
|
|
120
|
+
) -> JSONResponse:
|
|
121
|
+
return safe_error("INVALID_INPUT", "Document 请求格式无效", 400, False)
|
|
122
|
+
|
|
123
|
+
@app.exception_handler(SafeDocumentError)
|
|
124
|
+
async def safe_document_error(
|
|
125
|
+
_request: object, error: SafeDocumentError
|
|
126
|
+
) -> JSONResponse:
|
|
127
|
+
status = 403 if error.code == "FORBIDDEN" else 400
|
|
128
|
+
return safe_error(error.code, error.safe_message, status, error.retryable)
|
|
129
|
+
|
|
130
|
+
return app
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def safe_error(code: str, message: str, status: int, retryable: bool) -> JSONResponse:
|
|
134
|
+
return JSONResponse(
|
|
135
|
+
ErrorResponse(
|
|
136
|
+
code=code,
|
|
137
|
+
message=message,
|
|
138
|
+
request_id=f"document_{uuid.uuid4().hex}",
|
|
139
|
+
retryable=retryable,
|
|
140
|
+
).model_dump(by_alias=True),
|
|
141
|
+
status_code=status,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_event(event: dict[str, object]) -> str:
|
|
146
|
+
import json
|
|
147
|
+
|
|
148
|
+
return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
app = create_app(DocumentRuntimeConfig.from_environment(dict(os.environ)))
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class DocumentRuntimeConfig:
|
|
10
|
+
internal_token: str
|
|
11
|
+
store_path: str
|
|
12
|
+
artifact_url: str
|
|
13
|
+
adapter_url: str | None
|
|
14
|
+
adapter_token: str | None
|
|
15
|
+
deployment_key: str | None
|
|
16
|
+
capabilities: tuple[str, ...]
|
|
17
|
+
max_units: int = 500
|
|
18
|
+
connect_timeout_seconds: float = 5
|
|
19
|
+
request_timeout_seconds: float = 300
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_environment(cls, environment: dict[str, str]) -> DocumentRuntimeConfig:
|
|
23
|
+
internal_token = required(environment, "DOCUMENT_SERVER_INTERNAL_TOKEN")
|
|
24
|
+
store_path = environment.get(
|
|
25
|
+
"DOCUMENT_RUN_STORE_PATH", "/app/data/document/runs.sqlite3"
|
|
26
|
+
).strip()
|
|
27
|
+
artifact_url = environment.get(
|
|
28
|
+
"DOCUMENT_ARTIFACT_URL",
|
|
29
|
+
"http://server:9747/api/internal/ai/artifacts",
|
|
30
|
+
).strip()
|
|
31
|
+
validate_intranet_url(artifact_url)
|
|
32
|
+
adapter_url = environment.get("DOCUMENT_ADAPTER_URL", "").strip() or None
|
|
33
|
+
if adapter_url:
|
|
34
|
+
validate_intranet_url(adapter_url)
|
|
35
|
+
capabilities = tuple(
|
|
36
|
+
sorted(
|
|
37
|
+
{
|
|
38
|
+
item.strip()
|
|
39
|
+
for item in environment.get(
|
|
40
|
+
"DOCUMENT_ADAPTER_CAPABILITIES",
|
|
41
|
+
"document.ocr,document.layout,document.table",
|
|
42
|
+
).split(",")
|
|
43
|
+
if item.strip()
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return cls(
|
|
48
|
+
internal_token=internal_token,
|
|
49
|
+
store_path=store_path,
|
|
50
|
+
artifact_url=artifact_url,
|
|
51
|
+
adapter_url=adapter_url,
|
|
52
|
+
adapter_token=environment.get("DOCUMENT_ADAPTER_TOKEN", "").strip() or None,
|
|
53
|
+
deployment_key=environment.get("DOCUMENT_DEPLOYMENT_KEY", "").strip()
|
|
54
|
+
or None,
|
|
55
|
+
capabilities=capabilities,
|
|
56
|
+
max_units=positive_int(environment, "DOCUMENT_MAX_UNITS", 500),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def required(environment: dict[str, str], key: str) -> str:
|
|
61
|
+
value = environment.get(key, "").strip()
|
|
62
|
+
if not value:
|
|
63
|
+
raise ValueError(f"缺少 {key}")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def positive_int(environment: dict[str, str], key: str, default: int) -> int:
|
|
68
|
+
raw = environment.get(key, str(default)).strip()
|
|
69
|
+
try:
|
|
70
|
+
value = int(raw)
|
|
71
|
+
except ValueError:
|
|
72
|
+
raise ValueError(f"{key} 必须是正整数") from None
|
|
73
|
+
if value <= 0:
|
|
74
|
+
raise ValueError(f"{key} 必须是正整数")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_intranet_url(raw: str) -> None:
|
|
79
|
+
parsed = urlparse(raw)
|
|
80
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
81
|
+
raise ValueError("Document Adapter URL 无效")
|
|
82
|
+
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
83
|
+
raise ValueError("Document Adapter URL 不允许凭据、查询串或片段")
|
|
84
|
+
host = parsed.hostname.lower()
|
|
85
|
+
if host == "localhost" or host.endswith(".internal") or "." not in host:
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
address = ipaddress.ip_address(host)
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"Document Adapter 必须使用 localhost、内部服务名、私网 IP 或 .internal"
|
|
92
|
+
) from None
|
|
93
|
+
if not (address.is_private or address.is_loopback):
|
|
94
|
+
raise ValueError("Document Adapter 禁止使用公网地址")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Self
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
type JsonValue = (
|
|
8
|
+
str | int | float | bool | None | list[JsonValue] | dict[str, JsonValue]
|
|
9
|
+
)
|
|
10
|
+
type RunStatus = Literal["accepted", "running", "completed", "failed", "cancelled"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_camel(value: str) -> str:
|
|
14
|
+
first, *rest = value.split("_")
|
|
15
|
+
return first + "".join(part.capitalize() for part in rest)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CanonicalModel(BaseModel):
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
alias_generator=to_camel,
|
|
21
|
+
populate_by_name=True,
|
|
22
|
+
extra="forbid",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ArtifactInput(CanonicalModel):
|
|
27
|
+
value: str = Field(min_length=1, max_length=4096)
|
|
28
|
+
media_type: str = Field(min_length=1, max_length=160)
|
|
29
|
+
byte_size: int = Field(ge=0, le=512 * 1024 * 1024)
|
|
30
|
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RuntimeLimits(CanonicalModel):
|
|
34
|
+
max_duration_seconds: int = Field(ge=1, le=3600)
|
|
35
|
+
max_input_bytes: int = Field(ge=1, le=512 * 1024 * 1024)
|
|
36
|
+
max_output_bytes: int = Field(ge=1024, le=64 * 1024 * 1024)
|
|
37
|
+
max_units: int = Field(ge=1, le=10_000)
|
|
38
|
+
max_artifacts: int = Field(ge=1, le=100)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProviderEgress(CanonicalModel):
|
|
42
|
+
mode: Literal["forbidden", "configured-endpoint"]
|
|
43
|
+
deployment_key: str | None = None
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def validate_deployment(self) -> Self:
|
|
47
|
+
if self.mode == "configured-endpoint":
|
|
48
|
+
if not self.deployment_key or not self.deployment_key.strip():
|
|
49
|
+
raise ValueError("Provider Deployment Key 缺失")
|
|
50
|
+
elif self.deployment_key is not None:
|
|
51
|
+
raise ValueError("禁止出网时不能指定 Provider Deployment")
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProviderFieldPolicy(CanonicalModel):
|
|
56
|
+
field: Literal["artifact-content", "provider-output"]
|
|
57
|
+
classification: Literal["public", "internal", "sensitive"]
|
|
58
|
+
dlp_action: Literal["reject", "redact"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProviderInputFieldPolicy(CanonicalModel):
|
|
62
|
+
json_pointer: str = Field(pattern=r"^/[^/]+(?:/[^/]+)*$")
|
|
63
|
+
classification: Literal["public", "internal", "sensitive"]
|
|
64
|
+
dlp_action: Literal["reject", "redact"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ProviderDataPolicy(CanonicalModel):
|
|
68
|
+
input_fields: list[ProviderInputFieldPolicy] = Field(min_length=1)
|
|
69
|
+
envelope_fields: list[ProviderFieldPolicy] = Field(min_length=2)
|
|
70
|
+
|
|
71
|
+
@model_validator(mode="after")
|
|
72
|
+
def validate_fields(self) -> Self:
|
|
73
|
+
fields = [rule.field for rule in self.envelope_fields]
|
|
74
|
+
if set(fields) != {"artifact-content", "provider-output"}:
|
|
75
|
+
raise ValueError("Document Provider 字段治理策略不完整")
|
|
76
|
+
if len(fields) != len(set(fields)):
|
|
77
|
+
raise ValueError("Document Provider 字段治理策略重复")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class StartDocumentRunRequest(CanonicalModel):
|
|
82
|
+
kind: Literal["document"]
|
|
83
|
+
operation_key: str = Field(min_length=1, max_length=160)
|
|
84
|
+
options: JsonValue
|
|
85
|
+
sources: list[ArtifactInput] = Field(min_length=1, max_length=100)
|
|
86
|
+
required_capabilities: list[str] = Field(min_length=1, max_length=32)
|
|
87
|
+
data_classification: Literal["public", "internal", "sensitive"]
|
|
88
|
+
limits: RuntimeLimits
|
|
89
|
+
provider_egress: ProviderEgress
|
|
90
|
+
provider_data_policy: ProviderDataPolicy | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StartRunResponse(CanonicalModel):
|
|
94
|
+
run_id: str
|
|
95
|
+
status: RunStatus
|
|
96
|
+
events_url: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ErrorResponse(CanonicalModel):
|
|
100
|
+
code: str
|
|
101
|
+
message: str
|
|
102
|
+
request_id: str
|
|
103
|
+
retryable: bool
|