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,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import EmbeddingConfig
|
|
10
|
+
from .contracts import (
|
|
11
|
+
EmbeddingItem,
|
|
12
|
+
EmbeddingMetadata,
|
|
13
|
+
EmbeddingRequest,
|
|
14
|
+
EmbeddingResponse,
|
|
15
|
+
EmbeddingVector,
|
|
16
|
+
JsonValue,
|
|
17
|
+
)
|
|
18
|
+
from .error import SafeEmbeddingError
|
|
19
|
+
from .image_input import load_image
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmbeddingAdapter:
|
|
23
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
24
|
+
self.config = config
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def available(self) -> bool:
|
|
28
|
+
return self.config.adapter_url is not None
|
|
29
|
+
|
|
30
|
+
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
31
|
+
self._authorize(request)
|
|
32
|
+
if not self.config.adapter_url:
|
|
33
|
+
raise SafeEmbeddingError(
|
|
34
|
+
"CAPABILITY_UNAVAILABLE",
|
|
35
|
+
"Embedding Adapter 未配置",
|
|
36
|
+
True,
|
|
37
|
+
)
|
|
38
|
+
if len(request.items) > min(request.limits.max_units, self.config.max_units):
|
|
39
|
+
raise budget("Embedding 批量数量超过 Runtime 上限")
|
|
40
|
+
missing = set(request.required_capabilities) - set(self.config.capabilities)
|
|
41
|
+
if missing:
|
|
42
|
+
raise SafeEmbeddingError(
|
|
43
|
+
"CAPABILITY_UNAVAILABLE",
|
|
44
|
+
"Embedding Deployment 缺少 Operation 所需能力",
|
|
45
|
+
True,
|
|
46
|
+
)
|
|
47
|
+
prepared, input_hashes = await self._prepare(request)
|
|
48
|
+
raw = await self._invoke(request, prepared)
|
|
49
|
+
vectors = validate_vectors(
|
|
50
|
+
raw,
|
|
51
|
+
request.items,
|
|
52
|
+
input_hashes,
|
|
53
|
+
self.config.dimensions,
|
|
54
|
+
self.config.normalized,
|
|
55
|
+
)
|
|
56
|
+
return EmbeddingResponse(
|
|
57
|
+
batch_id=batch_id(request, input_hashes, self.config),
|
|
58
|
+
items=vectors,
|
|
59
|
+
metadata=EmbeddingMetadata(
|
|
60
|
+
deployment_alias=self.config.deployment_alias,
|
|
61
|
+
model=self.config.model,
|
|
62
|
+
version=self.config.version,
|
|
63
|
+
dimensions=self.config.dimensions,
|
|
64
|
+
normalized=self.config.normalized,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def _prepare(
|
|
69
|
+
self,
|
|
70
|
+
request: EmbeddingRequest,
|
|
71
|
+
) -> tuple[list[dict[str, JsonValue]], list[str]]:
|
|
72
|
+
artifacts = sum(item.source is not None for item in request.items)
|
|
73
|
+
if artifacts > (request.limits.max_artifacts or 0):
|
|
74
|
+
raise budget("Embedding 图片数量超过 Operation 上限")
|
|
75
|
+
if artifacts and request.limits.max_pixels_per_artifact is None:
|
|
76
|
+
raise budget("Embedding Operation 未声明图片像素上限")
|
|
77
|
+
prepared: list[dict[str, JsonValue]] = []
|
|
78
|
+
hashes: list[str] = []
|
|
79
|
+
remaining = request.limits.max_input_bytes
|
|
80
|
+
for item in request.items:
|
|
81
|
+
if item.text is not None:
|
|
82
|
+
body = item.text.encode()
|
|
83
|
+
remaining -= len(body)
|
|
84
|
+
if remaining < 0:
|
|
85
|
+
raise budget("Embedding 文本超过输入上限")
|
|
86
|
+
hashes.append(hashlib.sha256(body).hexdigest())
|
|
87
|
+
prepared.append({"id": item.id, "text": item.text})
|
|
88
|
+
continue
|
|
89
|
+
if item.source is None or request.limits.max_pixels_per_artifact is None:
|
|
90
|
+
raise invalid("Embedding 图片输入无效")
|
|
91
|
+
encoded, digest = await load_image(
|
|
92
|
+
item.source,
|
|
93
|
+
self.config,
|
|
94
|
+
remaining,
|
|
95
|
+
request.limits.max_pixels_per_artifact,
|
|
96
|
+
)
|
|
97
|
+
remaining -= item.source.byte_size
|
|
98
|
+
hashes.append(digest)
|
|
99
|
+
prepared.append(
|
|
100
|
+
{
|
|
101
|
+
"id": item.id,
|
|
102
|
+
"image": {
|
|
103
|
+
"mediaType": "image/png",
|
|
104
|
+
"dataBase64": encoded,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
return prepared, hashes
|
|
109
|
+
|
|
110
|
+
async def _invoke(
|
|
111
|
+
self,
|
|
112
|
+
request: EmbeddingRequest,
|
|
113
|
+
items: list[dict[str, JsonValue]],
|
|
114
|
+
) -> object:
|
|
115
|
+
url = self.config.adapter_url
|
|
116
|
+
if not url:
|
|
117
|
+
raise SafeEmbeddingError(
|
|
118
|
+
"CAPABILITY_UNAVAILABLE",
|
|
119
|
+
"Embedding Adapter 未配置",
|
|
120
|
+
True,
|
|
121
|
+
)
|
|
122
|
+
headers = (
|
|
123
|
+
{"authorization": f"Bearer {self.config.adapter_token}"}
|
|
124
|
+
if self.config.adapter_token
|
|
125
|
+
else {}
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
async with httpx.AsyncClient(
|
|
129
|
+
timeout=httpx.Timeout(
|
|
130
|
+
self.config.request_timeout_seconds,
|
|
131
|
+
connect=self.config.connect_timeout_seconds,
|
|
132
|
+
)
|
|
133
|
+
) as client:
|
|
134
|
+
response = await client.post(
|
|
135
|
+
f"{url.rstrip('/')}/v1/embeddings",
|
|
136
|
+
headers=headers,
|
|
137
|
+
json={
|
|
138
|
+
"deploymentAlias": self.config.deployment_alias,
|
|
139
|
+
"model": self.config.model,
|
|
140
|
+
"version": self.config.version,
|
|
141
|
+
"dimensions": self.config.dimensions,
|
|
142
|
+
"items": items,
|
|
143
|
+
"options": request.options,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
if len(response.content) > request.limits.max_output_bytes:
|
|
148
|
+
raise budget("Embedding 输出超过上限")
|
|
149
|
+
return response.json()
|
|
150
|
+
except SafeEmbeddingError:
|
|
151
|
+
raise
|
|
152
|
+
except Exception as error:
|
|
153
|
+
raise SafeEmbeddingError(
|
|
154
|
+
"RUNTIME_UNAVAILABLE",
|
|
155
|
+
"Embedding Adapter 执行失败",
|
|
156
|
+
True,
|
|
157
|
+
) from error
|
|
158
|
+
|
|
159
|
+
def _authorize(self, request: EmbeddingRequest) -> None:
|
|
160
|
+
if request.provider_egress.mode == "forbidden":
|
|
161
|
+
return
|
|
162
|
+
fields = (
|
|
163
|
+
request.provider_data_policy.envelope_fields
|
|
164
|
+
if request.provider_data_policy
|
|
165
|
+
else []
|
|
166
|
+
)
|
|
167
|
+
if (
|
|
168
|
+
not request.provider_data_policy
|
|
169
|
+
or not self.config.deployment_key
|
|
170
|
+
or request.provider_egress.deployment_key != self.config.deployment_key
|
|
171
|
+
or not any(item.get("field") == "embedding-input" for item in fields)
|
|
172
|
+
):
|
|
173
|
+
raise SafeEmbeddingError(
|
|
174
|
+
"FORBIDDEN",
|
|
175
|
+
"Embedding Operation 未授权当前 Deployment",
|
|
176
|
+
False,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_vectors(
|
|
181
|
+
value: object,
|
|
182
|
+
inputs: list[EmbeddingItem],
|
|
183
|
+
hashes: list[str],
|
|
184
|
+
dimensions: int,
|
|
185
|
+
normalized: bool,
|
|
186
|
+
) -> list[EmbeddingVector]:
|
|
187
|
+
vectors = value.get("vectors") if isinstance(value, dict) else None
|
|
188
|
+
if not isinstance(vectors, list) or len(vectors) != len(inputs):
|
|
189
|
+
raise invalid_output()
|
|
190
|
+
result: list[EmbeddingVector] = []
|
|
191
|
+
for index, (raw, expected) in enumerate(zip(vectors, inputs, strict=True)):
|
|
192
|
+
vector = raw.get("vector") if isinstance(raw, dict) else None
|
|
193
|
+
if (
|
|
194
|
+
not isinstance(raw, dict)
|
|
195
|
+
or raw.get("id") != expected.id
|
|
196
|
+
or not isinstance(vector, list)
|
|
197
|
+
or len(vector) != dimensions
|
|
198
|
+
or any(
|
|
199
|
+
not isinstance(item, int | float)
|
|
200
|
+
or isinstance(item, bool)
|
|
201
|
+
or not math.isfinite(item)
|
|
202
|
+
for item in vector
|
|
203
|
+
)
|
|
204
|
+
):
|
|
205
|
+
raise invalid_output()
|
|
206
|
+
values = [float(item) for item in vector]
|
|
207
|
+
norm = math.sqrt(sum(item * item for item in values))
|
|
208
|
+
if normalized and abs(norm - 1) > 0.01:
|
|
209
|
+
raise invalid_output("Embedding 向量未按声明归一化")
|
|
210
|
+
result.append(
|
|
211
|
+
EmbeddingVector(
|
|
212
|
+
id=expected.id,
|
|
213
|
+
vector=values,
|
|
214
|
+
input_sha256=hashes[index],
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def batch_id(
|
|
221
|
+
request: EmbeddingRequest,
|
|
222
|
+
hashes: list[str],
|
|
223
|
+
config: EmbeddingConfig,
|
|
224
|
+
) -> str:
|
|
225
|
+
payload = json.dumps(
|
|
226
|
+
{
|
|
227
|
+
"operationKey": request.operation_key,
|
|
228
|
+
"ids": [item.id for item in request.items],
|
|
229
|
+
"hashes": hashes,
|
|
230
|
+
"options": request.options,
|
|
231
|
+
"deploymentAlias": config.deployment_alias,
|
|
232
|
+
"model": config.model,
|
|
233
|
+
"version": config.version,
|
|
234
|
+
"dimensions": config.dimensions,
|
|
235
|
+
},
|
|
236
|
+
ensure_ascii=False,
|
|
237
|
+
separators=(",", ":"),
|
|
238
|
+
sort_keys=True,
|
|
239
|
+
).encode()
|
|
240
|
+
return f"embed_{hashlib.sha256(payload).hexdigest()[:32]}"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def invalid(message: str) -> SafeEmbeddingError:
|
|
244
|
+
return SafeEmbeddingError("INVALID_INPUT", message, False)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def budget(message: str) -> SafeEmbeddingError:
|
|
248
|
+
return SafeEmbeddingError("BUDGET_EXCEEDED", message, False)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def invalid_output(
|
|
252
|
+
message: str = "Embedding Adapter 输出无效",
|
|
253
|
+
) -> SafeEmbeddingError:
|
|
254
|
+
return SafeEmbeddingError("OUTPUT_VALIDATION_FAILED", message, False)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from fastapi import Depends, FastAPI, Header, HTTPException
|
|
9
|
+
from fastapi.exceptions import RequestValidationError
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
from .adapter import EmbeddingAdapter
|
|
13
|
+
from .config import EmbeddingConfig
|
|
14
|
+
from .contracts import EmbeddingRequest, EmbeddingResponse, ErrorResponse
|
|
15
|
+
from .error import SafeEmbeddingError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_app(
|
|
19
|
+
config: EmbeddingConfig,
|
|
20
|
+
adapter: EmbeddingAdapter | None = None,
|
|
21
|
+
) -> FastAPI:
|
|
22
|
+
app = FastAPI(title="Southwind Embedding Runtime", docs_url=None, redoc_url=None)
|
|
23
|
+
runtime = adapter or EmbeddingAdapter(config)
|
|
24
|
+
|
|
25
|
+
def authorize(authorization: str | None = Header(default=None)) -> None:
|
|
26
|
+
provided = (authorization or "").removeprefix("Bearer ")
|
|
27
|
+
if not provided or not hmac.compare_digest(provided, config.internal_token):
|
|
28
|
+
raise HTTPException(status_code=403, detail="FORBIDDEN")
|
|
29
|
+
|
|
30
|
+
@app.get("/health/ready")
|
|
31
|
+
async def ready(_: None = Depends(authorize)) -> JSONResponse:
|
|
32
|
+
if not runtime.available:
|
|
33
|
+
return JSONResponse(
|
|
34
|
+
{"status": "unavailable", "capabilities": []},
|
|
35
|
+
status_code=503,
|
|
36
|
+
)
|
|
37
|
+
return JSONResponse(
|
|
38
|
+
{
|
|
39
|
+
"status": "ready",
|
|
40
|
+
"capabilities": list(config.capabilities),
|
|
41
|
+
"limits": {"maxUnits": config.max_units},
|
|
42
|
+
"deployment": {
|
|
43
|
+
"alias": config.deployment_alias,
|
|
44
|
+
"model": config.model,
|
|
45
|
+
"version": config.version,
|
|
46
|
+
"dimensions": config.dimensions,
|
|
47
|
+
"normalized": config.normalized,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@app.post("/api/embeddings")
|
|
53
|
+
async def embed(
|
|
54
|
+
request: EmbeddingRequest,
|
|
55
|
+
idempotency_key: str = Header(alias="idempotency-key"),
|
|
56
|
+
_: None = Depends(authorize),
|
|
57
|
+
) -> EmbeddingResponse:
|
|
58
|
+
if not idempotency_key.strip() or len(idempotency_key) > 160:
|
|
59
|
+
raise HTTPException(status_code=400, detail="INVALID_INPUT")
|
|
60
|
+
try:
|
|
61
|
+
async with asyncio.timeout(request.limits.max_duration_seconds):
|
|
62
|
+
return await runtime.embed(request)
|
|
63
|
+
except TimeoutError as error:
|
|
64
|
+
raise SafeEmbeddingError(
|
|
65
|
+
"RUNTIME_UNAVAILABLE",
|
|
66
|
+
"Embedding 执行超时",
|
|
67
|
+
True,
|
|
68
|
+
) from error
|
|
69
|
+
|
|
70
|
+
@app.exception_handler(HTTPException)
|
|
71
|
+
async def http_error(_request: object, error: HTTPException) -> JSONResponse:
|
|
72
|
+
code = str(error.detail)
|
|
73
|
+
return safe_error(code, "Embedding 请求被拒绝", error.status_code, False)
|
|
74
|
+
|
|
75
|
+
@app.exception_handler(RequestValidationError)
|
|
76
|
+
async def validation_error(
|
|
77
|
+
_request: object,
|
|
78
|
+
_error: RequestValidationError,
|
|
79
|
+
) -> JSONResponse:
|
|
80
|
+
return safe_error("INVALID_INPUT", "Embedding 请求格式无效", 400, False)
|
|
81
|
+
|
|
82
|
+
@app.exception_handler(SafeEmbeddingError)
|
|
83
|
+
async def embedding_error(
|
|
84
|
+
_request: object,
|
|
85
|
+
error: SafeEmbeddingError,
|
|
86
|
+
) -> JSONResponse:
|
|
87
|
+
return safe_error(
|
|
88
|
+
error.code,
|
|
89
|
+
error.safe_message,
|
|
90
|
+
error_status(error.code),
|
|
91
|
+
error.retryable,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return app
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def error_status(code: str) -> int:
|
|
98
|
+
if code == "FORBIDDEN":
|
|
99
|
+
return 403
|
|
100
|
+
if code == "RUN_CONFLICT":
|
|
101
|
+
return 409
|
|
102
|
+
if code in {"CAPABILITY_UNAVAILABLE", "RUNTIME_UNAVAILABLE"}:
|
|
103
|
+
return 503
|
|
104
|
+
return 400
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def safe_error(code: str, message: str, status: int, retryable: bool) -> JSONResponse:
|
|
108
|
+
return JSONResponse(
|
|
109
|
+
ErrorResponse(
|
|
110
|
+
code=code,
|
|
111
|
+
message=message,
|
|
112
|
+
request_id=f"embedding_{uuid.uuid4().hex}",
|
|
113
|
+
retryable=retryable,
|
|
114
|
+
).model_dump(by_alias=True),
|
|
115
|
+
status_code=status,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
app = create_app(EmbeddingConfig.from_environment(dict(os.environ)))
|
|
@@ -0,0 +1,120 @@
|
|
|
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 EmbeddingConfig:
|
|
10
|
+
internal_token: str
|
|
11
|
+
artifact_url: str
|
|
12
|
+
adapter_url: str | None
|
|
13
|
+
adapter_token: str | None
|
|
14
|
+
deployment_key: str | None
|
|
15
|
+
deployment_alias: str
|
|
16
|
+
model: str
|
|
17
|
+
version: str
|
|
18
|
+
dimensions: int
|
|
19
|
+
normalized: bool
|
|
20
|
+
capabilities: tuple[str, ...]
|
|
21
|
+
max_units: int = 256
|
|
22
|
+
connect_timeout_seconds: float = 5
|
|
23
|
+
request_timeout_seconds: float = 120
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_environment(cls, environment: dict[str, str]) -> EmbeddingConfig:
|
|
27
|
+
artifact_url = environment.get(
|
|
28
|
+
"EMBEDDING_ARTIFACT_URL",
|
|
29
|
+
"http://server:9747/api/internal/ai/artifacts",
|
|
30
|
+
).strip()
|
|
31
|
+
validate_intranet_url(artifact_url)
|
|
32
|
+
adapter_url = environment.get("EMBEDDING_ADAPTER_URL", "").strip() or None
|
|
33
|
+
if adapter_url:
|
|
34
|
+
validate_intranet_url(adapter_url)
|
|
35
|
+
return cls(
|
|
36
|
+
internal_token=required(environment, "EMBEDDING_SERVER_INTERNAL_TOKEN"),
|
|
37
|
+
artifact_url=artifact_url,
|
|
38
|
+
adapter_url=adapter_url,
|
|
39
|
+
adapter_token=environment.get("EMBEDDING_ADAPTER_TOKEN", "").strip()
|
|
40
|
+
or None,
|
|
41
|
+
deployment_key=environment.get("EMBEDDING_DEPLOYMENT_KEY", "").strip()
|
|
42
|
+
or None,
|
|
43
|
+
deployment_alias=required(environment, "EMBEDDING_DEPLOYMENT_ALIAS"),
|
|
44
|
+
model=required(environment, "EMBEDDING_MODEL"),
|
|
45
|
+
version=required(environment, "EMBEDDING_MODEL_VERSION"),
|
|
46
|
+
dimensions=required_positive_int(environment, "EMBEDDING_DIMENSIONS"),
|
|
47
|
+
normalized=boolean(environment, "EMBEDDING_NORMALIZED", True),
|
|
48
|
+
capabilities=required_capabilities(
|
|
49
|
+
tuple(
|
|
50
|
+
sorted(
|
|
51
|
+
{
|
|
52
|
+
item.strip()
|
|
53
|
+
for item in environment.get(
|
|
54
|
+
"EMBEDDING_CAPABILITIES",
|
|
55
|
+
"embedding.text",
|
|
56
|
+
).split(",")
|
|
57
|
+
if item.strip()
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
),
|
|
62
|
+
max_units=positive_int(environment, "EMBEDDING_MAX_UNITS", 256),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def required(environment: dict[str, str], key: str) -> str:
|
|
67
|
+
value = environment.get(key, "").strip()
|
|
68
|
+
if not value:
|
|
69
|
+
raise ValueError(f"缺少 {key}")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def positive_int(environment: dict[str, str], key: str, default: int) -> int:
|
|
74
|
+
raw = environment.get(key, str(default)).strip()
|
|
75
|
+
try:
|
|
76
|
+
value = int(raw)
|
|
77
|
+
except ValueError:
|
|
78
|
+
raise ValueError(f"{key} 必须是正整数") from None
|
|
79
|
+
if value <= 0:
|
|
80
|
+
raise ValueError(f"{key} 必须是正整数")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def required_positive_int(environment: dict[str, str], key: str) -> int:
|
|
85
|
+
return positive_int(environment, key, 0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def boolean(environment: dict[str, str], key: str, default: bool) -> bool:
|
|
89
|
+
raw = environment.get(key, str(default).lower()).strip().lower()
|
|
90
|
+
if raw not in {"true", "false"}:
|
|
91
|
+
raise ValueError(f"{key} 必须是 true 或 false")
|
|
92
|
+
return raw == "true"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def required_capabilities(value: tuple[str, ...]) -> tuple[str, ...]:
|
|
96
|
+
if not value:
|
|
97
|
+
raise ValueError("EMBEDDING_CAPABILITIES 不能为空")
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_intranet_url(raw: str) -> None:
|
|
102
|
+
parsed = urlparse(raw)
|
|
103
|
+
if (
|
|
104
|
+
parsed.scheme not in {"http", "https"}
|
|
105
|
+
or not parsed.hostname
|
|
106
|
+
or parsed.username
|
|
107
|
+
or parsed.password
|
|
108
|
+
or parsed.query
|
|
109
|
+
or parsed.fragment
|
|
110
|
+
):
|
|
111
|
+
raise ValueError("Embedding 内部 URL 无效")
|
|
112
|
+
host = parsed.hostname.lower()
|
|
113
|
+
if host == "localhost" or host.endswith(".internal") or "." not in host:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
address = ipaddress.ip_address(host)
|
|
117
|
+
except ValueError:
|
|
118
|
+
raise ValueError("Embedding 内部 URL 必须使用本地或私网地址") from None
|
|
119
|
+
if not (address.is_private or address.is_loopback):
|
|
120
|
+
raise ValueError("Embedding 内部 URL 禁止使用公网地址")
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
|
|
11
|
+
|
|
12
|
+
def to_camel(value: str) -> str:
|
|
13
|
+
first, *rest = value.split("_")
|
|
14
|
+
return first + "".join(part.capitalize() for part in rest)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CanonicalModel(BaseModel):
|
|
18
|
+
model_config = ConfigDict(
|
|
19
|
+
alias_generator=to_camel,
|
|
20
|
+
populate_by_name=True,
|
|
21
|
+
extra="forbid",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ArtifactInput(CanonicalModel):
|
|
26
|
+
value: str = Field(min_length=1)
|
|
27
|
+
media_type: str = Field(min_length=1)
|
|
28
|
+
byte_size: int = Field(ge=1)
|
|
29
|
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EmbeddingItem(CanonicalModel):
|
|
33
|
+
id: str = Field(min_length=1, max_length=200)
|
|
34
|
+
text: str | None = None
|
|
35
|
+
source: ArtifactInput | None = None
|
|
36
|
+
|
|
37
|
+
@model_validator(mode="after")
|
|
38
|
+
def exactly_one_input(self) -> Self:
|
|
39
|
+
if (self.text is None) == (self.source is None):
|
|
40
|
+
raise ValueError("Embedding Item 必须且只能包含一种输入")
|
|
41
|
+
if self.text is not None and not self.text.strip():
|
|
42
|
+
raise ValueError("Embedding 文本不能为空")
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RuntimeLimits(CanonicalModel):
|
|
47
|
+
max_duration_seconds: int = Field(ge=1, le=600)
|
|
48
|
+
max_input_bytes: int = Field(ge=1, le=268_435_456)
|
|
49
|
+
max_output_bytes: int = Field(ge=1, le=268_435_456)
|
|
50
|
+
max_units: int = Field(ge=1, le=256)
|
|
51
|
+
max_artifacts: int | None = Field(default=None, ge=1, le=16)
|
|
52
|
+
max_pixels_per_artifact: int | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
ge=1,
|
|
55
|
+
le=100_000_000,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProviderEgress(CanonicalModel):
|
|
60
|
+
mode: Literal["forbidden", "configured-endpoint"]
|
|
61
|
+
deployment_key: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProviderDataPolicy(CanonicalModel):
|
|
65
|
+
input_fields: list[dict[str, JsonValue]]
|
|
66
|
+
envelope_fields: list[dict[str, JsonValue]]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EmbeddingRequest(CanonicalModel):
|
|
70
|
+
kind: Literal["embedding"]
|
|
71
|
+
operation_key: str = Field(min_length=1)
|
|
72
|
+
items: list[EmbeddingItem] = Field(min_length=1, max_length=256)
|
|
73
|
+
options: JsonValue
|
|
74
|
+
input_modalities: list[Literal["text", "image"]] = Field(min_length=1)
|
|
75
|
+
required_capabilities: list[str] = Field(min_length=1, max_length=32)
|
|
76
|
+
data_classification: Literal["public", "internal", "sensitive"]
|
|
77
|
+
limits: RuntimeLimits
|
|
78
|
+
provider_egress: ProviderEgress
|
|
79
|
+
provider_data_policy: ProviderDataPolicy | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class EmbeddingVector(CanonicalModel):
|
|
83
|
+
id: str
|
|
84
|
+
vector: list[float]
|
|
85
|
+
input_sha256: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class EmbeddingMetadata(CanonicalModel):
|
|
89
|
+
deployment_alias: str
|
|
90
|
+
model: str
|
|
91
|
+
version: str
|
|
92
|
+
dimensions: int
|
|
93
|
+
normalized: bool
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class EmbeddingResponse(CanonicalModel):
|
|
97
|
+
batch_id: str
|
|
98
|
+
items: list[EmbeddingVector]
|
|
99
|
+
metadata: EmbeddingMetadata
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ErrorResponse(CanonicalModel):
|
|
103
|
+
code: str
|
|
104
|
+
message: str
|
|
105
|
+
request_id: str
|
|
106
|
+
retryable: bool
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from PIL import Image, UnidentifiedImageError
|
|
10
|
+
|
|
11
|
+
from .config import EmbeddingConfig
|
|
12
|
+
from .contracts import ArtifactInput
|
|
13
|
+
from .error import SafeEmbeddingError
|
|
14
|
+
|
|
15
|
+
_IMAGE_TYPES = frozenset({"image/jpeg", "image/png", "image/webp"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def load_image(
|
|
19
|
+
source: ArtifactInput,
|
|
20
|
+
config: EmbeddingConfig,
|
|
21
|
+
max_bytes: int,
|
|
22
|
+
max_pixels: int,
|
|
23
|
+
) -> tuple[str, str]:
|
|
24
|
+
if source.media_type.lower() not in _IMAGE_TYPES:
|
|
25
|
+
raise invalid("Embedding 图片格式不受支持")
|
|
26
|
+
body = bytearray()
|
|
27
|
+
digest = hashlib.sha256()
|
|
28
|
+
try:
|
|
29
|
+
async with httpx.AsyncClient(
|
|
30
|
+
timeout=httpx.Timeout(
|
|
31
|
+
config.request_timeout_seconds,
|
|
32
|
+
connect=config.connect_timeout_seconds,
|
|
33
|
+
)
|
|
34
|
+
) as client:
|
|
35
|
+
async with client.stream(
|
|
36
|
+
"GET",
|
|
37
|
+
config.artifact_url,
|
|
38
|
+
headers={
|
|
39
|
+
"authorization": f"Bearer {config.internal_token}",
|
|
40
|
+
"x-ai-artifact-ref": source.value,
|
|
41
|
+
},
|
|
42
|
+
) as response:
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
async for chunk in response.aiter_bytes():
|
|
45
|
+
body.extend(chunk)
|
|
46
|
+
digest.update(chunk)
|
|
47
|
+
if len(body) > min(source.byte_size, max_bytes):
|
|
48
|
+
raise budget("Embedding 图片超过输入上限")
|
|
49
|
+
except SafeEmbeddingError:
|
|
50
|
+
raise
|
|
51
|
+
except Exception as error:
|
|
52
|
+
raise SafeEmbeddingError(
|
|
53
|
+
"ARTIFACT_UNAVAILABLE",
|
|
54
|
+
"Embedding Runtime 无法读取图片 Artifact",
|
|
55
|
+
True,
|
|
56
|
+
) from error
|
|
57
|
+
if len(body) != source.byte_size or digest.hexdigest() != source.sha256:
|
|
58
|
+
raise invalid("Embedding Artifact 完整性校验失败")
|
|
59
|
+
encoded = await asyncio.to_thread(sanitize, bytes(body), max_pixels)
|
|
60
|
+
return encoded, source.sha256
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sanitize(raw: bytes, max_pixels: int) -> str:
|
|
64
|
+
try:
|
|
65
|
+
with Image.open(BytesIO(raw)) as source:
|
|
66
|
+
width, height = source.size
|
|
67
|
+
if width <= 0 or height <= 0 or width * height > max_pixels:
|
|
68
|
+
raise budget("Embedding 图片像素超过上限")
|
|
69
|
+
source.load()
|
|
70
|
+
normalized = source.convert(
|
|
71
|
+
"RGBA" if source.has_transparency_data else "RGB"
|
|
72
|
+
)
|
|
73
|
+
target = BytesIO()
|
|
74
|
+
normalized.save(target, format="PNG", optimize=False)
|
|
75
|
+
except SafeEmbeddingError:
|
|
76
|
+
raise
|
|
77
|
+
except (Image.DecompressionBombError, UnidentifiedImageError, OSError):
|
|
78
|
+
raise invalid("Embedding 图片内容损坏") from None
|
|
79
|
+
return base64.b64encode(target.getvalue()).decode()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def invalid(message: str) -> SafeEmbeddingError:
|
|
83
|
+
return SafeEmbeddingError("INVALID_INPUT", message, False)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def budget(message: str) -> SafeEmbeddingError:
|
|
87
|
+
return SafeEmbeddingError("BUDGET_EXCEEDED", message, False)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|