@open-mercato/ai-assistant 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +361 -0
- package/README.md +5 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools.js +14 -0
- package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +78 -12
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +228 -0
- package/dist/modules/ai_assistant/data/entities.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +33 -0
- package/dist/modules/ai_assistant/events.js.map +7 -0
- package/dist/modules/ai_assistant/i18n/de.json +252 -0
- package/dist/modules/ai_assistant/i18n/en.json +252 -0
- package/dist/modules/ai_assistant/i18n/es.json +252 -0
- package/dist/modules/ai_assistant/i18n/pl.json +252 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
- package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +36 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
- package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
- package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
- package/generated/entities/ai_pending_action/index.ts +24 -0
- package/generated/entities.ids.generated.ts +13 -0
- package/generated/entity-fields-registry.ts +57 -0
- package/jest.config.cjs +7 -0
- package/package.json +4 -4
- package/src/index.ts +215 -0
- package/src/modules/ai_assistant/__integration__/README.md +5 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
- package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
- package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
- package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
- package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
- package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
- package/src/modules/ai_assistant/ai-tools.ts +21 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
- package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +99 -24
- package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
- package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
- package/src/modules/ai_assistant/data/entities.ts +270 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
- package/src/modules/ai_assistant/events.ts +112 -0
- package/src/modules/ai_assistant/i18n/de.json +252 -0
- package/src/modules/ai_assistant/i18n/en.json +252 -0
- package/src/modules/ai_assistant/i18n/es.json +252 -0
- package/src/modules/ai_assistant/i18n/pl.json +252 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
- package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
- package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
- package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
- package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
- package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
- package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
- package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
- package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
- package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
- package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
- package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
- package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
- package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
- package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
- package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
- package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
- package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
- package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
- package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
- package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
- package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
- package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
- package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
- package/src/modules/ai_assistant/lib/types.ts +105 -2
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
- package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
- package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
- package/src/modules/ai_assistant/setup.ts +53 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
- package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
|
|
3
|
+
test.describe("TC-AI-PLAYGROUND-004: AI Playground", () => {
|
|
4
|
+
const playgroundPath = "/backend/config/ai-assistant/playground";
|
|
5
|
+
test("playground renders agent picker or empty state for superadmin", async ({ page }) => {
|
|
6
|
+
await login(page, "superadmin");
|
|
7
|
+
await page.route("**/api/ai_assistant/ai/agents", async (route) => {
|
|
8
|
+
await route.fulfill({
|
|
9
|
+
status: 200,
|
|
10
|
+
contentType: "application/json",
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
agents: [
|
|
13
|
+
{
|
|
14
|
+
id: "customers.assistant",
|
|
15
|
+
moduleId: "customers",
|
|
16
|
+
label: "Customers assistant",
|
|
17
|
+
description: "Answers questions about customer records.",
|
|
18
|
+
executionMode: "chat",
|
|
19
|
+
mutationPolicy: "read-only",
|
|
20
|
+
allowedTools: ["customers.list_people"],
|
|
21
|
+
requiredFeatures: [],
|
|
22
|
+
acceptedMediaTypes: [],
|
|
23
|
+
hasOutputSchema: false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "catalog.extract",
|
|
27
|
+
moduleId: "catalog",
|
|
28
|
+
label: "Catalog extractor",
|
|
29
|
+
description: "Extracts structured product metadata.",
|
|
30
|
+
executionMode: "object",
|
|
31
|
+
mutationPolicy: "read-only",
|
|
32
|
+
allowedTools: [],
|
|
33
|
+
requiredFeatures: [],
|
|
34
|
+
acceptedMediaTypes: [],
|
|
35
|
+
hasOutputSchema: true
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
total: 2
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
await page.route("**/api/ai_assistant/ai/chat**", async (route) => {
|
|
43
|
+
await route.fulfill({
|
|
44
|
+
status: 200,
|
|
45
|
+
contentType: "text/event-stream",
|
|
46
|
+
body: "ok from stubbed SSE\n"
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
await page.route("**/api/ai_assistant/ai/run-object", async (route) => {
|
|
50
|
+
await route.fulfill({
|
|
51
|
+
status: 200,
|
|
52
|
+
contentType: "application/json",
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
object: { title: "Stubbed title" },
|
|
55
|
+
finishReason: "stop",
|
|
56
|
+
usage: { inputTokens: 1, outputTokens: 2 }
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
|
|
61
|
+
const container = page.locator("[data-ai-playground]");
|
|
62
|
+
const empty = page.getByText(/No AI agents are registered/i).first();
|
|
63
|
+
const loadError = page.locator("[data-ai-playground-error]");
|
|
64
|
+
await expect(container.or(empty).or(loadError)).toBeVisible({ timeout: 15e3 });
|
|
65
|
+
if (await container.isVisible().catch(() => false)) {
|
|
66
|
+
const picker = page.locator("[data-ai-playground-agent-picker]");
|
|
67
|
+
await expect(picker).toBeVisible();
|
|
68
|
+
const optionCount = await picker.locator("option").count();
|
|
69
|
+
expect(optionCount).toBeGreaterThanOrEqual(2);
|
|
70
|
+
const debugToggle = page.locator("[data-ai-playground-debug-toggle]");
|
|
71
|
+
await expect(debugToggle).toBeVisible();
|
|
72
|
+
const composer = page.locator("#ai-chat-composer");
|
|
73
|
+
await expect(composer).toBeVisible();
|
|
74
|
+
await composer.fill("Hello from Playwright");
|
|
75
|
+
await expect(composer).toHaveValue("Hello from Playwright");
|
|
76
|
+
const debugPanel = page.locator('[data-ai-chat-debug="true"]');
|
|
77
|
+
const initiallyVisible = await debugPanel.isVisible().catch(() => false);
|
|
78
|
+
if (!initiallyVisible) {
|
|
79
|
+
await debugToggle.click();
|
|
80
|
+
}
|
|
81
|
+
await expect(debugPanel).toBeVisible({ timeout: 5e3 });
|
|
82
|
+
await expect(
|
|
83
|
+
page.locator('[data-ai-chat-debug-section="tools"]')
|
|
84
|
+
).toBeVisible();
|
|
85
|
+
await expect(
|
|
86
|
+
page.locator('[data-ai-chat-debug-section="promptSections"]')
|
|
87
|
+
).toBeVisible();
|
|
88
|
+
await expect(
|
|
89
|
+
page.locator('[data-ai-chat-debug-section="lastRequest"]')
|
|
90
|
+
).toBeVisible();
|
|
91
|
+
} else if (await empty.isVisible().catch(() => false)) {
|
|
92
|
+
await expect(empty).toBeVisible();
|
|
93
|
+
} else {
|
|
94
|
+
await expect(loadError).toBeVisible();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
test("picker lists all three Phase 2 agents and chat-mode selection disables object-mode tab", async ({
|
|
98
|
+
page
|
|
99
|
+
}) => {
|
|
100
|
+
test.setTimeout(12e4);
|
|
101
|
+
await login(page, "superadmin");
|
|
102
|
+
await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
|
|
103
|
+
const container = page.locator("[data-ai-playground]");
|
|
104
|
+
await expect(container).toBeVisible({ timeout: 6e4 });
|
|
105
|
+
const picker = page.locator("[data-ai-playground-agent-picker]");
|
|
106
|
+
await expect(picker).toBeVisible();
|
|
107
|
+
await expect(
|
|
108
|
+
picker.locator('option[value="customers.account_assistant"]')
|
|
109
|
+
).toHaveCount(1);
|
|
110
|
+
await expect(
|
|
111
|
+
picker.locator('option[value="catalog.catalog_assistant"]')
|
|
112
|
+
).toHaveCount(1);
|
|
113
|
+
await expect(
|
|
114
|
+
picker.locator('option[value="catalog.merchandising_assistant"]')
|
|
115
|
+
).toHaveCount(1);
|
|
116
|
+
await page.getByRole("tab", { name: /object mode/i }).click();
|
|
117
|
+
await expect(
|
|
118
|
+
page.locator('[data-ai-playground-unsupported="object"]')
|
|
119
|
+
).toBeVisible();
|
|
120
|
+
await page.getByRole("tab", { name: /^chat$/i }).click();
|
|
121
|
+
const chatRegion = page.locator("[data-ai-chat-agent]").first();
|
|
122
|
+
await expect(chatRegion).toBeVisible();
|
|
123
|
+
});
|
|
124
|
+
test("chat happy path \u2014 stubbed SSE response appears in the transcript", async ({ page }) => {
|
|
125
|
+
test.setTimeout(12e4);
|
|
126
|
+
await login(page, "superadmin");
|
|
127
|
+
await page.route("**/api/ai_assistant/ai/agents", async (route) => {
|
|
128
|
+
await route.fulfill({
|
|
129
|
+
status: 200,
|
|
130
|
+
contentType: "application/json",
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
agents: [
|
|
133
|
+
{
|
|
134
|
+
id: "customers.account_assistant",
|
|
135
|
+
moduleId: "customers",
|
|
136
|
+
label: "Customers account assistant",
|
|
137
|
+
description: "Stubbed agent for playground chat smoke.",
|
|
138
|
+
executionMode: "chat",
|
|
139
|
+
mutationPolicy: "read-only",
|
|
140
|
+
allowedTools: [],
|
|
141
|
+
requiredFeatures: [],
|
|
142
|
+
acceptedMediaTypes: [],
|
|
143
|
+
hasOutputSchema: false
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
total: 1
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const streamBody = [
|
|
151
|
+
"event: text",
|
|
152
|
+
'data: {"content":"stubbed-playground-reply"}',
|
|
153
|
+
"",
|
|
154
|
+
"event: done",
|
|
155
|
+
"data: {}",
|
|
156
|
+
""
|
|
157
|
+
].join("\n");
|
|
158
|
+
await page.route("**/api/ai_assistant/ai/chat**", async (route) => {
|
|
159
|
+
await route.fulfill({
|
|
160
|
+
status: 200,
|
|
161
|
+
contentType: "text/event-stream",
|
|
162
|
+
body: streamBody
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
|
|
166
|
+
const composer = page.locator("#ai-chat-composer");
|
|
167
|
+
await expect(composer).toBeVisible({ timeout: 6e4 });
|
|
168
|
+
await composer.fill("Say hi please");
|
|
169
|
+
await composer.press("Meta+Enter");
|
|
170
|
+
await page.waitForTimeout(200);
|
|
171
|
+
if (!await page.locator('[data-ai-chat-state="thinking"]').count()) {
|
|
172
|
+
await composer.press("Control+Enter");
|
|
173
|
+
}
|
|
174
|
+
await expect(async () => {
|
|
175
|
+
const cleared = await composer.inputValue() === "";
|
|
176
|
+
const rendered = await page.getByText(/stubbed-playground-reply/i).count();
|
|
177
|
+
const thinking = await page.locator('[data-ai-chat-state="thinking"]').count();
|
|
178
|
+
expect(cleared || rendered > 0 || thinking > 0).toBe(true);
|
|
179
|
+
}).toPass({ timeout: 1e4 });
|
|
180
|
+
});
|
|
181
|
+
test("mutation-preview-card renders inside the playground transcript when a pending action is emitted", async ({
|
|
182
|
+
page
|
|
183
|
+
}) => {
|
|
184
|
+
test.setTimeout(12e4);
|
|
185
|
+
await login(page, "superadmin");
|
|
186
|
+
await page.route("**/api/ai_assistant/ai/agents", async (route) => {
|
|
187
|
+
await route.fulfill({
|
|
188
|
+
status: 200,
|
|
189
|
+
contentType: "application/json",
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
agents: [
|
|
192
|
+
{
|
|
193
|
+
id: "customers.account_assistant",
|
|
194
|
+
moduleId: "customers",
|
|
195
|
+
label: "Customers account assistant",
|
|
196
|
+
description: "Mutation-capable agent for Phase 3 approval flow.",
|
|
197
|
+
executionMode: "chat",
|
|
198
|
+
mutationPolicy: "require-approval",
|
|
199
|
+
allowedTools: ["customers.update_person"],
|
|
200
|
+
requiredFeatures: [],
|
|
201
|
+
acceptedMediaTypes: [],
|
|
202
|
+
hasOutputSchema: false
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
total: 1
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
const pendingRow = {
|
|
210
|
+
pendingAction: {
|
|
211
|
+
id: "pa-stub-001",
|
|
212
|
+
agentId: "customers.account_assistant",
|
|
213
|
+
toolName: "customers.update_person",
|
|
214
|
+
status: "pending",
|
|
215
|
+
fieldDiff: [
|
|
216
|
+
{ field: "name", before: "Alice", after: "Alicia" }
|
|
217
|
+
],
|
|
218
|
+
records: null,
|
|
219
|
+
failedRecords: null,
|
|
220
|
+
sideEffectsSummary: "Rename Alice to Alicia.",
|
|
221
|
+
attachmentIds: [],
|
|
222
|
+
targetEntityType: "customers.person",
|
|
223
|
+
targetRecordId: "p-1",
|
|
224
|
+
recordVersion: "1",
|
|
225
|
+
executionResult: null,
|
|
226
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
227
|
+
expiresAt: new Date(Date.now() + 6e5).toISOString(),
|
|
228
|
+
resolvedAt: null,
|
|
229
|
+
resolvedByUserId: null
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
await page.route("**/api/ai_assistant/ai/actions/pa-stub-001", async (route) => {
|
|
233
|
+
await route.fulfill({
|
|
234
|
+
status: 200,
|
|
235
|
+
contentType: "application/json",
|
|
236
|
+
body: JSON.stringify(pendingRow)
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
await page.goto(
|
|
240
|
+
`${playgroundPath}?uiPart=mutation-preview-card&pendingActionId=pa-stub-001`,
|
|
241
|
+
{ waitUntil: "domcontentloaded" }
|
|
242
|
+
);
|
|
243
|
+
const container = page.locator("[data-ai-playground]");
|
|
244
|
+
await expect(container).toBeVisible({ timeout: 6e4 });
|
|
245
|
+
const previewCard = page.locator("[data-ai-mutation-preview]").first();
|
|
246
|
+
await expect(previewCard).toBeVisible({ timeout: 15e3 });
|
|
247
|
+
await expect(page.locator("[data-ai-mutation-preview-confirm]")).toBeVisible();
|
|
248
|
+
await expect(page.locator("[data-ai-mutation-preview-cancel]")).toBeVisible();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
//# sourceMappingURL=TC-AI-PLAYGROUND-004-playground.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-PLAYGROUND-004: AI Playground page smoke (Step 4.4 / Phase 2 WS-B).\n *\n * Covers the first user-facing embedding of `<AiChat>` in the backoffice. The\n * route is guarded by `ai_assistant.settings.manage`; superadmin always holds\n * it. The agent registry may be empty in CI (generated file depends on\n * module authors registering agents), so the spec handles BOTH branches:\n * - populated registry: assert the agent picker, debug toggle, and chat\n * surface render;\n * - empty registry: assert the `EmptyState` copy renders instead.\n *\n * The chat SSE response is stubbed via Playwright's route interception so the\n * test never has to hit a live LLM provider. The object-mode run-object route\n * is also stubbed to assert wiring without invoking a real model.\n */\ntest.describe('TC-AI-PLAYGROUND-004: AI Playground', () => {\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n test('playground renders agent picker or empty state for superadmin', async ({ page }) => {\n await login(page, 'superadmin');\n\n // Stub the agents list so the test behavior is deterministic regardless of\n // what the generated registry holds at run-time.\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agents: [\n {\n id: 'customers.assistant',\n moduleId: 'customers',\n label: 'Customers assistant',\n description: 'Answers questions about customer records.',\n executionMode: 'chat',\n mutationPolicy: 'read-only',\n allowedTools: ['customers.list_people'],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n {\n id: 'catalog.extract',\n moduleId: 'catalog',\n label: 'Catalog extractor',\n description: 'Extracts structured product metadata.',\n executionMode: 'object',\n mutationPolicy: 'read-only',\n allowedTools: [],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: true,\n },\n ],\n total: 2,\n }),\n });\n });\n\n // Stub the chat SSE so the composer has something to \"succeed\" against.\n await page.route('**/api/ai_assistant/ai/chat**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'text/event-stream',\n body: 'ok from stubbed SSE\\n',\n });\n });\n\n // Stub the run-object dispatcher so the object-mode tab has a deterministic response.\n await page.route('**/api/ai_assistant/ai/run-object', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n object: { title: 'Stubbed title' },\n finishReason: 'stop',\n usage: { inputTokens: 1, outputTokens: 2 },\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const container = page.locator('[data-ai-playground]');\n const empty = page.getByText(/No AI agents are registered/i).first();\n const loadError = page.locator('[data-ai-playground-error]');\n\n // Wait until the page has stabilized (either container, empty state, or a load error).\n await expect(container.or(empty).or(loadError)).toBeVisible({ timeout: 15_000 });\n\n if (await container.isVisible().catch(() => false)) {\n const picker = page.locator('[data-ai-playground-agent-picker]');\n await expect(picker).toBeVisible();\n // Two stubbed agents should surface.\n const optionCount = await picker.locator('option').count();\n expect(optionCount).toBeGreaterThanOrEqual(2);\n\n const debugToggle = page.locator('[data-ai-playground-debug-toggle]');\n await expect(debugToggle).toBeVisible();\n\n const composer = page.locator('#ai-chat-composer');\n await expect(composer).toBeVisible();\n await composer.fill('Hello from Playwright');\n await expect(composer).toHaveValue('Hello from Playwright');\n\n // Step 4.6: toggling the debug panel should reveal the three collapsible\n // sections (tool map / prompt sections / last request), each addressable\n // by its `data-ai-chat-debug-section` attribute.\n const debugPanel = page.locator('[data-ai-chat-debug=\"true\"]');\n const initiallyVisible = await debugPanel.isVisible().catch(() => false);\n if (!initiallyVisible) {\n await debugToggle.click();\n }\n await expect(debugPanel).toBeVisible({ timeout: 5_000 });\n await expect(\n page.locator('[data-ai-chat-debug-section=\"tools\"]'),\n ).toBeVisible();\n await expect(\n page.locator('[data-ai-chat-debug-section=\"promptSections\"]'),\n ).toBeVisible();\n await expect(\n page.locator('[data-ai-chat-debug-section=\"lastRequest\"]'),\n ).toBeVisible();\n } else if (await empty.isVisible().catch(() => false)) {\n // Empty branch: agent registry is empty in this environment.\n await expect(empty).toBeVisible();\n } else {\n // Fatal load error branch \u2014 still acceptable evidence that the guard fired.\n await expect(loadError).toBeVisible();\n }\n });\n\n test('picker lists all three Phase 2 agents and chat-mode selection disables object-mode tab', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n // Hit the live registry for this scenario \u2014 Phase 2 agents are all\n // chat-mode so we can assert the \"not supported\" alert on the object\n // tab without stubbing.\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const container = page.locator('[data-ai-playground]');\n await expect(container).toBeVisible({ timeout: 60_000 });\n\n const picker = page.locator('[data-ai-playground-agent-picker]');\n await expect(picker).toBeVisible();\n\n // The three Phase 2 agents must be present.\n await expect(\n picker.locator('option[value=\"customers.account_assistant\"]'),\n ).toHaveCount(1);\n await expect(\n picker.locator('option[value=\"catalog.catalog_assistant\"]'),\n ).toHaveCount(1);\n await expect(\n picker.locator('option[value=\"catalog.merchandising_assistant\"]'),\n ).toHaveCount(1);\n\n // Switch to the object-mode tab \u2014 Phase 2 agents are all chat-mode, so\n // the \"not supported\" info Alert should render with the documented\n // `data-ai-playground-unsupported=\"object\"` marker.\n await page.getByRole('tab', { name: /object mode/i }).click();\n await expect(\n page.locator('[data-ai-playground-unsupported=\"object\"]'),\n ).toBeVisible();\n\n // Flip back to chat tab; the `AiChat` region for the currently selected\n // agent must render.\n await page.getByRole('tab', { name: /^chat$/i }).click();\n const chatRegion = page.locator('[data-ai-chat-agent]').first();\n await expect(chatRegion).toBeVisible();\n });\n\n test('chat happy path \u2014 stubbed SSE response appears in the transcript', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n // Stub agents so the picker is deterministic.\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agents: [\n {\n id: 'customers.account_assistant',\n moduleId: 'customers',\n label: 'Customers account assistant',\n description: 'Stubbed agent for playground chat smoke.',\n executionMode: 'chat',\n mutationPolicy: 'read-only',\n allowedTools: [],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n }),\n });\n });\n\n // Stub the SSE dispatcher with a canned text stream the AiChat reader\n // consumes. The AiChat component writes streamed deltas into the\n // transcript; we assert the text surfaces regardless of the exact\n // stream dialect by filling in both a `data:` framed payload and a\n // plain-text fallback.\n const streamBody = [\n 'event: text',\n 'data: {\"content\":\"stubbed-playground-reply\"}',\n '',\n 'event: done',\n 'data: {}',\n '',\n ].join('\\n');\n await page.route('**/api/ai_assistant/ai/chat**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'text/event-stream',\n body: streamBody,\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const composer = page.locator('#ai-chat-composer');\n await expect(composer).toBeVisible({ timeout: 60_000 });\n await composer.fill('Say hi please');\n await composer.press('Meta+Enter');\n // On non-mac runners Meta+Enter is a no-op; try Control+Enter too.\n await page.waitForTimeout(200);\n if (!(await page.locator('[data-ai-chat-state=\"thinking\"]').count())) {\n await composer.press('Control+Enter');\n }\n\n // The composer should have been cleared on submit, proving the\n // handler fired. The SSE stream body itself is driven by the agent\n // runtime which may surface the text either verbatim or through a\n // rendered message row \u2014 accept either signal.\n await expect(async () => {\n const cleared = (await composer.inputValue()) === '';\n const rendered = await page.getByText(/stubbed-playground-reply/i).count();\n const thinking = await page.locator('[data-ai-chat-state=\"thinking\"]').count();\n expect(cleared || rendered > 0 || thinking > 0).toBe(true);\n }).toPass({ timeout: 10_000 });\n });\n\n test('mutation-preview-card renders inside the playground transcript when a pending action is emitted', async ({\n page,\n }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n // Deterministic agent registry \u2014 a mutation-capable chat agent.\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agents: [\n {\n id: 'customers.account_assistant',\n moduleId: 'customers',\n label: 'Customers account assistant',\n description: 'Mutation-capable agent for Phase 3 approval flow.',\n executionMode: 'chat',\n mutationPolicy: 'require-approval',\n allowedTools: ['customers.update_person'],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n }),\n });\n });\n\n // Stub the polling endpoint with a pending row so the preview card\n // resolves its state. The Playwright test never hits a real DB.\n const pendingRow = {\n pendingAction: {\n id: 'pa-stub-001',\n agentId: 'customers.account_assistant',\n toolName: 'customers.update_person',\n status: 'pending',\n fieldDiff: [\n { field: 'name', before: 'Alice', after: 'Alicia' },\n ],\n records: null,\n failedRecords: null,\n sideEffectsSummary: 'Rename Alice to Alicia.',\n attachmentIds: [],\n targetEntityType: 'customers.person',\n targetRecordId: 'p-1',\n recordVersion: '1',\n executionResult: null,\n createdAt: new Date().toISOString(),\n expiresAt: new Date(Date.now() + 600_000).toISOString(),\n resolvedAt: null,\n resolvedByUserId: null,\n },\n };\n await page.route('**/api/ai_assistant/ai/actions/pa-stub-001', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(pendingRow),\n });\n });\n\n // Navigate with the debug seed that instructs the playground to inject\n // the `mutation-preview-card` UI part. This is the Step 5.10 stub path\n // until the dispatcher surfaces UI parts through the streamed body.\n await page.goto(\n `${playgroundPath}?uiPart=mutation-preview-card&pendingActionId=pa-stub-001`,\n { waitUntil: 'domcontentloaded' },\n );\n\n const container = page.locator('[data-ai-playground]');\n await expect(container).toBeVisible({ timeout: 60_000 });\n\n const previewCard = page.locator('[data-ai-mutation-preview]').first();\n await expect(previewCard).toBeVisible({ timeout: 15_000 });\n await expect(page.locator('[data-ai-mutation-preview-confirm]')).toBeVisible();\n await expect(page.locator('[data-ai-mutation-preview-cancel]')).toBeVisible();\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { expect, test } from "@playwright/test";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const REPORT_BEGIN = "---TOOL_TEST_REPORT_BEGIN---";
|
|
8
|
+
const REPORT_END = "---TOOL_TEST_REPORT_END---";
|
|
9
|
+
function findRepoRoot() {
|
|
10
|
+
return path.resolve(__dirname, "..", "..", "..", "..", "..", "..");
|
|
11
|
+
}
|
|
12
|
+
function findAppRoot() {
|
|
13
|
+
const testAppRoot = process.env.OM_TEST_APP_ROOT;
|
|
14
|
+
if (testAppRoot && testAppRoot.length > 0) {
|
|
15
|
+
return testAppRoot;
|
|
16
|
+
}
|
|
17
|
+
return path.join(findRepoRoot(), "apps", "mercato");
|
|
18
|
+
}
|
|
19
|
+
function runToolTestsCli() {
|
|
20
|
+
return new Promise((resolveCmd, rejectCmd) => {
|
|
21
|
+
const cwd = findAppRoot();
|
|
22
|
+
const child = spawn(
|
|
23
|
+
"yarn",
|
|
24
|
+
["mercato", "ai_assistant", "test-tools", "--json"],
|
|
25
|
+
{
|
|
26
|
+
cwd,
|
|
27
|
+
env: { ...process.env, FORCE_COLOR: "0", NODE_NO_WARNINGS: "1" },
|
|
28
|
+
shell: false
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
let stdout = "";
|
|
32
|
+
let stderr = "";
|
|
33
|
+
child.stdout.on("data", (chunk) => {
|
|
34
|
+
stdout += chunk.toString();
|
|
35
|
+
});
|
|
36
|
+
child.stderr.on("data", (chunk) => {
|
|
37
|
+
stderr += chunk.toString();
|
|
38
|
+
});
|
|
39
|
+
child.on("error", (err) => rejectCmd(err));
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
resolveCmd({ stdout, stderr, code: code ?? -1 });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function parseReport(stdout) {
|
|
46
|
+
const beginIdx = stdout.indexOf(REPORT_BEGIN);
|
|
47
|
+
const endIdx = stdout.indexOf(REPORT_END);
|
|
48
|
+
if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Could not find report markers in CLI output. stdout (first 500 chars):
|
|
51
|
+
${stdout.slice(0, 500)}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const payload = stdout.slice(beginIdx + REPORT_BEGIN.length, endIdx).trim();
|
|
55
|
+
return JSON.parse(payload);
|
|
56
|
+
}
|
|
57
|
+
test.describe("TC-INT-AI-TOOLS: Every AI tool returns the expected shape", () => {
|
|
58
|
+
test("all registered tools either pass or skip with a reason; none fail", async () => {
|
|
59
|
+
test.slow();
|
|
60
|
+
const { stdout, stderr, code } = await runToolTestsCli();
|
|
61
|
+
if (code !== 0 && stdout.indexOf(REPORT_BEGIN) === -1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`tool-test CLI failed (exit ${code}) before producing a report.
|
|
64
|
+
stderr:
|
|
65
|
+
${stderr.slice(0, 2e3)}
|
|
66
|
+
stdout:
|
|
67
|
+
${stdout.slice(0, 1e3)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const report = parseReport(stdout);
|
|
71
|
+
const failures = report.records.filter((r) => r.status === "fail");
|
|
72
|
+
const skips = report.records.filter((r) => r.status === "skip");
|
|
73
|
+
if (failures.length > 0) {
|
|
74
|
+
console.log(
|
|
75
|
+
`[TC-INT-AI-TOOLS] Failures (${failures.length}):
|
|
76
|
+
${failures.map((r) => ` - ${r.tool}: ${r.reason ?? "<no reason>"}`).join("\n")}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (skips.length > 0) {
|
|
80
|
+
console.log(
|
|
81
|
+
`[TC-INT-AI-TOOLS] Skips (${skips.length}): ${skips.map((r) => `${r.tool} (${r.reason ?? "skipped"})`).join(", ")}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
console.log(
|
|
85
|
+
`[TC-INT-AI-TOOLS] Result: total=${report.total} pass=${report.passed} fail=${report.failed} skip=${report.skipped}`
|
|
86
|
+
);
|
|
87
|
+
expect(failures, `Expected zero failing AI tools, got ${failures.length}`).toEqual([]);
|
|
88
|
+
expect(report.passed, "At least one tool must run successfully").toBeGreaterThan(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=TC-INT-AI-TOOLS.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { spawn } from 'node:child_process'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { expect, test } from '@playwright/test'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\n/**\n * TC-INT-AI-TOOLS: Smoke-test every AI tool registered via `defineAiTool`.\n *\n * Strategy: shells out to `yarn mercato ai_assistant test-tools --json` which\n * runs the in-process tool runner (`packages/ai-assistant/.../lib/tool-test-runner.ts`).\n * The runner iterates every entry in `apps/mercato/.mercato/generated/ai-tools.generated.ts`,\n * invokes each handler with a small fixture input against a super-admin\n * context, and returns a structured report. Mutation tools are exercised\n * through `prepareMutation` only \u2014 the test asserts a pending-action envelope\n * is returned and never confirms the action.\n *\n * No HTTP endpoint is added; the runner is CLI-only and never exposed.\n */\n\ninterface ToolTestRecord {\n module: string\n tool: string\n isMutation: boolean\n status: 'pass' | 'fail' | 'skip'\n durationMs: number\n reason?: string\n}\n\ninterface ToolTestReport {\n tenantId: string | null\n organizationId: string | null\n total: number\n passed: number\n failed: number\n skipped: number\n records: ToolTestRecord[]\n}\n\nconst REPORT_BEGIN = '---TOOL_TEST_REPORT_BEGIN---'\nconst REPORT_END = '---TOOL_TEST_REPORT_END---'\n\nfunction findRepoRoot(): string {\n return path.resolve(__dirname, '..', '..', '..', '..', '..', '..')\n}\n\nfunction findAppRoot(): string {\n const testAppRoot = process.env.OM_TEST_APP_ROOT\n if (testAppRoot && testAppRoot.length > 0) {\n return testAppRoot\n }\n\n // The CLI loads its env from apps/mercato/.env (DB connection, JWT secret,\n // encryption fallback). Spawning from there makes the bootstrap reach a\n // ready state instead of bailing on the MFA-secret precondition.\n return path.join(findRepoRoot(), 'apps', 'mercato')\n}\n\nfunction runToolTestsCli(): Promise<{ stdout: string; stderr: string; code: number }> {\n return new Promise((resolveCmd, rejectCmd) => {\n const cwd = findAppRoot()\n const child = spawn(\n 'yarn',\n ['mercato', 'ai_assistant', 'test-tools', '--json'],\n {\n cwd,\n env: { ...process.env, FORCE_COLOR: '0', NODE_NO_WARNINGS: '1' },\n shell: false,\n },\n )\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => {\n stdout += chunk.toString()\n })\n child.stderr.on('data', (chunk) => {\n stderr += chunk.toString()\n })\n child.on('error', (err) => rejectCmd(err))\n child.on('close', (code) => {\n resolveCmd({ stdout, stderr, code: code ?? -1 })\n })\n })\n}\n\nfunction parseReport(stdout: string): ToolTestReport {\n const beginIdx = stdout.indexOf(REPORT_BEGIN)\n const endIdx = stdout.indexOf(REPORT_END)\n if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {\n throw new Error(\n `Could not find report markers in CLI output. stdout (first 500 chars):\\n${stdout.slice(0, 500)}`,\n )\n }\n const payload = stdout.slice(beginIdx + REPORT_BEGIN.length, endIdx).trim()\n return JSON.parse(payload) as ToolTestReport\n}\n\ntest.describe('TC-INT-AI-TOOLS: Every AI tool returns the expected shape', () => {\n test('all registered tools either pass or skip with a reason; none fail', async () => {\n test.slow()\n const { stdout, stderr, code } = await runToolTestsCli()\n if (code !== 0 && stdout.indexOf(REPORT_BEGIN) === -1) {\n throw new Error(\n `tool-test CLI failed (exit ${code}) before producing a report.\\nstderr:\\n${stderr.slice(0, 2000)}\\nstdout:\\n${stdout.slice(0, 1000)}`,\n )\n }\n const report = parseReport(stdout)\n\n // Surface a compact summary in the test log for triage.\n const failures = report.records.filter((r) => r.status === 'fail')\n const skips = report.records.filter((r) => r.status === 'skip')\n if (failures.length > 0) {\n console.log(\n `[TC-INT-AI-TOOLS] Failures (${failures.length}):\\n${failures\n .map((r) => ` - ${r.tool}: ${r.reason ?? '<no reason>'}`)\n .join('\\n')}`,\n )\n }\n if (skips.length > 0) {\n console.log(\n `[TC-INT-AI-TOOLS] Skips (${skips.length}): ${skips\n .map((r) => `${r.tool} (${r.reason ?? 'skipped'})`)\n .join(', ')}`,\n )\n }\n console.log(\n `[TC-INT-AI-TOOLS] Result: total=${report.total} pass=${report.passed} fail=${report.failed} skip=${report.skipped}`,\n )\n\n expect(failures, `Expected zero failing AI tools, got ${failures.length}`).toEqual([])\n expect(report.passed, 'At least one tool must run successfully').toBeGreaterThan(0)\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { defineAiTool } from "../lib/ai-tool-definition.js";
|
|
4
|
+
async function loadAttachmentEntity() {
|
|
5
|
+
const mod = await import("@open-mercato/core/modules/attachments/data/entities");
|
|
6
|
+
return mod.Attachment;
|
|
7
|
+
}
|
|
8
|
+
async function loadAttachmentMetadata() {
|
|
9
|
+
const mod = await import("@open-mercato/core/modules/attachments/lib/metadata");
|
|
10
|
+
return mod;
|
|
11
|
+
}
|
|
12
|
+
function assertTenantScope(ctx) {
|
|
13
|
+
if (!ctx.tenantId) {
|
|
14
|
+
throw new Error("Tenant context is required for attachments tools");
|
|
15
|
+
}
|
|
16
|
+
return ctx.tenantId;
|
|
17
|
+
}
|
|
18
|
+
function resolveEm(ctx) {
|
|
19
|
+
return ctx.container.resolve("em");
|
|
20
|
+
}
|
|
21
|
+
const listInput = z.object({
|
|
22
|
+
entityType: z.string().min(1).describe('Entity identifier (e.g. "customers:customer_person_profile").'),
|
|
23
|
+
recordId: z.string().min(1).describe("Record identifier within that entity.")
|
|
24
|
+
});
|
|
25
|
+
const listRecordAttachmentsTool = defineAiTool({
|
|
26
|
+
name: "attachments.list_record_attachments",
|
|
27
|
+
displayName: "List record attachments",
|
|
28
|
+
description: "List attachments bound to a record, scoped to the caller tenant and organization. Returns metadata only (no bytes, no signed URL).",
|
|
29
|
+
inputSchema: listInput,
|
|
30
|
+
requiredFeatures: ["attachments.view"],
|
|
31
|
+
tags: ["read", "attachments"],
|
|
32
|
+
handler: async (rawInput, ctx) => {
|
|
33
|
+
const tenantId = assertTenantScope(ctx);
|
|
34
|
+
const input = listInput.parse(rawInput);
|
|
35
|
+
const em = resolveEm(ctx);
|
|
36
|
+
const Attachment = await loadAttachmentEntity();
|
|
37
|
+
const where = {
|
|
38
|
+
entityId: input.entityType,
|
|
39
|
+
recordId: input.recordId,
|
|
40
|
+
tenantId
|
|
41
|
+
};
|
|
42
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId;
|
|
43
|
+
const rows = await findWithDecryption(
|
|
44
|
+
em,
|
|
45
|
+
Attachment,
|
|
46
|
+
where,
|
|
47
|
+
{ orderBy: { createdAt: "desc" } },
|
|
48
|
+
{ tenantId, organizationId: ctx.organizationId }
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
entityType: input.entityType,
|
|
52
|
+
recordId: input.recordId,
|
|
53
|
+
total: rows.length,
|
|
54
|
+
items: rows.map((row) => ({
|
|
55
|
+
id: row.id,
|
|
56
|
+
entityType: row.entityId,
|
|
57
|
+
recordId: row.recordId,
|
|
58
|
+
fileName: row.fileName,
|
|
59
|
+
mediaType: row.mimeType,
|
|
60
|
+
size: row.fileSize,
|
|
61
|
+
partitionCode: row.partitionCode,
|
|
62
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const readInput = z.object({
|
|
68
|
+
attachmentId: z.string().uuid().describe("Attachment identifier."),
|
|
69
|
+
includeExtractedText: z.boolean().optional().describe("When true, include the stored extracted / OCR text if present (default false).")
|
|
70
|
+
});
|
|
71
|
+
const readAttachmentTool = defineAiTool({
|
|
72
|
+
name: "attachments.read_attachment",
|
|
73
|
+
displayName: "Read attachment metadata",
|
|
74
|
+
description: "Return attachment metadata, tags, assignments, and optionally the stored extracted text. Never returns raw bytes or signed URLs.",
|
|
75
|
+
inputSchema: readInput,
|
|
76
|
+
requiredFeatures: ["attachments.view"],
|
|
77
|
+
tags: ["read", "attachments"],
|
|
78
|
+
handler: async (rawInput, ctx) => {
|
|
79
|
+
const tenantId = assertTenantScope(ctx);
|
|
80
|
+
const input = readInput.parse(rawInput);
|
|
81
|
+
const em = resolveEm(ctx);
|
|
82
|
+
const Attachment = await loadAttachmentEntity();
|
|
83
|
+
const { readAttachmentMetadata } = await loadAttachmentMetadata();
|
|
84
|
+
const where = { id: input.attachmentId, tenantId };
|
|
85
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId;
|
|
86
|
+
const row = await findOneWithDecryption(
|
|
87
|
+
em,
|
|
88
|
+
Attachment,
|
|
89
|
+
where,
|
|
90
|
+
void 0,
|
|
91
|
+
{ tenantId, organizationId: ctx.organizationId }
|
|
92
|
+
);
|
|
93
|
+
if (!row) {
|
|
94
|
+
return { found: false, attachmentId: input.attachmentId };
|
|
95
|
+
}
|
|
96
|
+
const metadata = readAttachmentMetadata(row.storageMetadata);
|
|
97
|
+
return {
|
|
98
|
+
found: true,
|
|
99
|
+
id: row.id,
|
|
100
|
+
entityType: row.entityId,
|
|
101
|
+
recordId: row.recordId,
|
|
102
|
+
fileName: row.fileName,
|
|
103
|
+
mediaType: row.mimeType,
|
|
104
|
+
size: row.fileSize,
|
|
105
|
+
partitionCode: row.partitionCode,
|
|
106
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
107
|
+
tags: metadata.tags ?? [],
|
|
108
|
+
assignments: metadata.assignments ?? [],
|
|
109
|
+
extractedText: input.includeExtractedText === true && typeof row.content === "string" ? row.content : null,
|
|
110
|
+
hasExtractedText: typeof row.content === "string" && row.content.length > 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const transferInput = z.object({
|
|
115
|
+
fromEntityType: z.string().min(1).describe("Current entity type of the source attachments."),
|
|
116
|
+
fromRecordId: z.string().min(1).describe("Current record id the attachments are bound to."),
|
|
117
|
+
toEntityType: z.string().min(1).describe("Target entity type (must match the source)."),
|
|
118
|
+
toRecordId: z.string().min(1).describe("Target record id to re-bind the attachments to."),
|
|
119
|
+
attachmentIds: z.array(z.string().uuid()).min(1).max(100).optional().describe("Optional subset; defaults to every attachment on the source record.")
|
|
120
|
+
});
|
|
121
|
+
const transferRecordAttachmentsTool = defineAiTool({
|
|
122
|
+
name: "attachments.transfer_record_attachments",
|
|
123
|
+
displayName: "Transfer record attachments",
|
|
124
|
+
description: "Move uploaded files from a temporary/draft record to a saved record. Mutation tool \u2014 agents with readOnly=true are blocked by the policy gate.",
|
|
125
|
+
inputSchema: transferInput,
|
|
126
|
+
isMutation: true,
|
|
127
|
+
requiredFeatures: ["attachments.manage"],
|
|
128
|
+
tags: ["write", "attachments"],
|
|
129
|
+
handler: async (rawInput, ctx) => {
|
|
130
|
+
const tenantId = assertTenantScope(ctx);
|
|
131
|
+
const input = transferInput.parse(rawInput);
|
|
132
|
+
if (input.fromEntityType !== input.toEntityType) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"attachments.transfer_record_attachments requires fromEntityType and toEntityType to match"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const em = resolveEm(ctx);
|
|
138
|
+
const Attachment = await loadAttachmentEntity();
|
|
139
|
+
const { readAttachmentMetadata, mergeAttachmentMetadata } = await loadAttachmentMetadata();
|
|
140
|
+
const where = {
|
|
141
|
+
entityId: input.fromEntityType,
|
|
142
|
+
recordId: input.fromRecordId,
|
|
143
|
+
tenantId
|
|
144
|
+
};
|
|
145
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId;
|
|
146
|
+
if (input.attachmentIds && input.attachmentIds.length > 0) {
|
|
147
|
+
where.id = { $in: input.attachmentIds };
|
|
148
|
+
}
|
|
149
|
+
const rows = await findWithDecryption(
|
|
150
|
+
em,
|
|
151
|
+
Attachment,
|
|
152
|
+
where,
|
|
153
|
+
void 0,
|
|
154
|
+
{ tenantId, organizationId: ctx.organizationId }
|
|
155
|
+
);
|
|
156
|
+
if (!rows.length) {
|
|
157
|
+
return {
|
|
158
|
+
transferred: 0,
|
|
159
|
+
fromEntityType: input.fromEntityType,
|
|
160
|
+
fromRecordId: input.fromRecordId,
|
|
161
|
+
toEntityType: input.toEntityType,
|
|
162
|
+
toRecordId: input.toRecordId
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
const previousRecordId = row.recordId;
|
|
167
|
+
row.recordId = input.toRecordId;
|
|
168
|
+
const metadata = readAttachmentMetadata(row.storageMetadata);
|
|
169
|
+
const nextAssignments = metadata.assignments?.map((assignment) => {
|
|
170
|
+
const matchesType = assignment.type === input.fromEntityType;
|
|
171
|
+
const matchesRecord = assignment.id === previousRecordId;
|
|
172
|
+
if (matchesType && matchesRecord) {
|
|
173
|
+
return { ...assignment, id: input.toRecordId };
|
|
174
|
+
}
|
|
175
|
+
return assignment;
|
|
176
|
+
}) ?? [];
|
|
177
|
+
row.storageMetadata = mergeAttachmentMetadata(row.storageMetadata, {
|
|
178
|
+
assignments: nextAssignments
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
await em.persist(rows).flush();
|
|
182
|
+
return {
|
|
183
|
+
transferred: rows.length,
|
|
184
|
+
fromEntityType: input.fromEntityType,
|
|
185
|
+
fromRecordId: input.fromRecordId,
|
|
186
|
+
toEntityType: input.toEntityType,
|
|
187
|
+
toRecordId: input.toRecordId,
|
|
188
|
+
attachmentIds: rows.map((row) => row.id)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
const attachmentsAiTools = [
|
|
193
|
+
listRecordAttachmentsTool,
|
|
194
|
+
readAttachmentTool,
|
|
195
|
+
transferRecordAttachmentsTool
|
|
196
|
+
];
|
|
197
|
+
var attachments_pack_default = attachmentsAiTools;
|
|
198
|
+
export {
|
|
199
|
+
attachmentsAiTools,
|
|
200
|
+
attachments_pack_default as default
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=attachments-pack.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/ai-tools/attachments-pack.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * General-purpose `attachments.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * Read-only tools return metadata + optional extracted text; the\n * attachment-to-model bridge (Step 3.7) owns raw bytes / signed URLs.\n * The transfer tool is the only mutation \u2014 agents with `readOnly: true`\n * are already filtered by the Step 3.2 policy gate.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition } from '../lib/types'\n\ntype AttachmentMetadataModule = {\n readAttachmentMetadata: (raw: unknown) => {\n tags?: string[]\n assignments?: Array<{ type: string; id: string; href?: string | null; label?: string | null }>\n }\n mergeAttachmentMetadata: (\n raw: unknown,\n patch: { assignments?: unknown; tags?: unknown },\n ) => Record<string, unknown>\n}\n\ntype AttachmentEntityModule = {\n Attachment: new () => unknown\n}\n\nasync function loadAttachmentEntity(): Promise<AttachmentEntityModule['Attachment']> {\n const mod = (await import(\n '@open-mercato/core/modules/attachments/data/entities'\n )) as AttachmentEntityModule\n return mod.Attachment\n}\n\nasync function loadAttachmentMetadata(): Promise<AttachmentMetadataModule> {\n const mod = (await import(\n '@open-mercato/core/modules/attachments/lib/metadata'\n )) as AttachmentMetadataModule\n return mod\n}\n\ntype AttachmentRow = {\n id: string\n entityId: string\n recordId: string\n fileName: string\n mimeType: string\n fileSize: number\n storageMetadata?: Record<string, unknown> | null\n url?: string\n content?: string | null\n tenantId?: string | null\n organizationId?: string | null\n partitionCode?: string\n createdAt?: Date\n}\n\nfunction assertTenantScope(ctx: { tenantId: string | null }): string {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for attachments tools')\n }\n return ctx.tenantId\n}\n\nfunction resolveEm(ctx: {\n container: { resolve: <T = unknown>(name: string) => T }\n}): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nconst listInput = z.object({\n entityType: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record identifier within that entity.'),\n})\n\nconst listRecordAttachmentsTool = defineAiTool({\n name: 'attachments.list_record_attachments',\n displayName: 'List record attachments',\n description:\n 'List attachments bound to a record, scoped to the caller tenant and organization. Returns metadata only (no bytes, no signed URL).',\n inputSchema: listInput,\n requiredFeatures: ['attachments.view'],\n tags: ['read', 'attachments'],\n handler: async (rawInput, ctx) => {\n const tenantId = assertTenantScope(ctx)\n const input = listInput.parse(rawInput)\n const em = resolveEm(ctx)\n const Attachment = await loadAttachmentEntity()\n const where: Record<string, unknown> = {\n entityId: input.entityType,\n recordId: input.recordId,\n tenantId,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const rows = (await findWithDecryption<AttachmentRow>(\n em,\n Attachment as unknown as new () => AttachmentRow,\n where,\n { orderBy: { createdAt: 'desc' } as any },\n { tenantId, organizationId: ctx.organizationId },\n )) as AttachmentRow[]\n return {\n entityType: input.entityType,\n recordId: input.recordId,\n total: rows.length,\n items: rows.map((row) => ({\n id: row.id,\n entityType: row.entityId,\n recordId: row.recordId,\n fileName: row.fileName,\n mediaType: row.mimeType,\n size: row.fileSize,\n partitionCode: row.partitionCode,\n createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,\n })),\n }\n },\n})\n\nconst readInput = z.object({\n attachmentId: z.string().uuid().describe('Attachment identifier.'),\n includeExtractedText: z\n .boolean()\n .optional()\n .describe('When true, include the stored extracted / OCR text if present (default false).'),\n})\n\nconst readAttachmentTool = defineAiTool({\n name: 'attachments.read_attachment',\n displayName: 'Read attachment metadata',\n description:\n 'Return attachment metadata, tags, assignments, and optionally the stored extracted text. Never returns raw bytes or signed URLs.',\n inputSchema: readInput,\n requiredFeatures: ['attachments.view'],\n tags: ['read', 'attachments'],\n handler: async (rawInput, ctx) => {\n const tenantId = assertTenantScope(ctx)\n const input = readInput.parse(rawInput)\n const em = resolveEm(ctx)\n const Attachment = await loadAttachmentEntity()\n const { readAttachmentMetadata } = await loadAttachmentMetadata()\n const where: Record<string, unknown> = { id: input.attachmentId, tenantId }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const row = (await findOneWithDecryption<AttachmentRow>(\n em,\n Attachment as unknown as new () => AttachmentRow,\n where,\n undefined,\n { tenantId, organizationId: ctx.organizationId },\n )) as AttachmentRow | null\n if (!row) {\n return { found: false as const, attachmentId: input.attachmentId }\n }\n const metadata = readAttachmentMetadata(row.storageMetadata)\n return {\n found: true as const,\n id: row.id,\n entityType: row.entityId,\n recordId: row.recordId,\n fileName: row.fileName,\n mediaType: row.mimeType,\n size: row.fileSize,\n partitionCode: row.partitionCode,\n createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n extractedText:\n input.includeExtractedText === true && typeof row.content === 'string' ? row.content : null,\n hasExtractedText: typeof row.content === 'string' && row.content.length > 0,\n }\n },\n})\n\nconst transferInput = z.object({\n fromEntityType: z.string().min(1).describe('Current entity type of the source attachments.'),\n fromRecordId: z.string().min(1).describe('Current record id the attachments are bound to.'),\n toEntityType: z.string().min(1).describe('Target entity type (must match the source).'),\n toRecordId: z.string().min(1).describe('Target record id to re-bind the attachments to.'),\n attachmentIds: z\n .array(z.string().uuid())\n .min(1)\n .max(100)\n .optional()\n .describe('Optional subset; defaults to every attachment on the source record.'),\n})\n\nconst transferRecordAttachmentsTool = defineAiTool({\n name: 'attachments.transfer_record_attachments',\n displayName: 'Transfer record attachments',\n description:\n 'Move uploaded files from a temporary/draft record to a saved record. Mutation tool \u2014 agents with readOnly=true are blocked by the policy gate.',\n inputSchema: transferInput,\n isMutation: true,\n requiredFeatures: ['attachments.manage'],\n tags: ['write', 'attachments'],\n handler: async (rawInput, ctx) => {\n const tenantId = assertTenantScope(ctx)\n const input = transferInput.parse(rawInput)\n if (input.fromEntityType !== input.toEntityType) {\n throw new Error(\n 'attachments.transfer_record_attachments requires fromEntityType and toEntityType to match',\n )\n }\n const em = resolveEm(ctx)\n const Attachment = await loadAttachmentEntity()\n const { readAttachmentMetadata, mergeAttachmentMetadata } = await loadAttachmentMetadata()\n const where: Record<string, unknown> = {\n entityId: input.fromEntityType,\n recordId: input.fromRecordId,\n tenantId,\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n if (input.attachmentIds && input.attachmentIds.length > 0) {\n where.id = { $in: input.attachmentIds }\n }\n const rows = (await findWithDecryption<AttachmentRow>(\n em,\n Attachment as unknown as new () => AttachmentRow,\n where,\n undefined,\n { tenantId, organizationId: ctx.organizationId },\n )) as AttachmentRow[]\n if (!rows.length) {\n return {\n transferred: 0,\n fromEntityType: input.fromEntityType,\n fromRecordId: input.fromRecordId,\n toEntityType: input.toEntityType,\n toRecordId: input.toRecordId,\n }\n }\n for (const row of rows) {\n const previousRecordId = row.recordId\n row.recordId = input.toRecordId\n const metadata = readAttachmentMetadata(row.storageMetadata)\n const nextAssignments =\n metadata.assignments?.map((assignment) => {\n const matchesType = assignment.type === input.fromEntityType\n const matchesRecord = assignment.id === previousRecordId\n if (matchesType && matchesRecord) {\n return { ...assignment, id: input.toRecordId }\n }\n return assignment\n }) ?? []\n row.storageMetadata = mergeAttachmentMetadata(row.storageMetadata, {\n assignments: nextAssignments,\n })\n }\n await em.persist(rows).flush()\n return {\n transferred: rows.length,\n fromEntityType: input.fromEntityType,\n fromRecordId: input.fromRecordId,\n toEntityType: input.toEntityType,\n toRecordId: input.toRecordId,\n attachmentIds: rows.map((row) => row.id),\n }\n },\n})\n\nexport const attachmentsAiTools: AiToolDefinition<any, any>[] = [\n listRecordAttachmentsTool,\n readAttachmentTool,\n transferRecordAttachmentsTool,\n]\n\nexport default attachmentsAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|