@open-mercato/ai-assistant 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
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 +1 -1
- package/dist/modules/ai_assistant/ai-tools/search-pack.js +3 -93
- package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +6 -14
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +2 -10
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +1 -3
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/in-process-client.js +1 -3
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js +1 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +1 -3
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +1 -3
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/package.json +7 -8
- package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +0 -5
- package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +5 -211
- package/src/modules/ai_assistant/ai-tools/search-pack.ts +4 -110
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -3
- package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +0 -16
- package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +0 -30
- package/src/modules/ai_assistant/lib/codemode-tools.ts +7 -21
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +2 -10
- package/src/modules/ai_assistant/lib/http-server.ts +0 -2
- package/src/modules/ai_assistant/lib/in-process-client.ts +0 -2
- package/src/modules/ai_assistant/lib/mcp-client.ts +0 -1
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +0 -2
- package/src/modules/ai_assistant/lib/mcp-server.ts +0 -2
- package/src/modules/ai_assistant/lib/types.ts +0 -11
- package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js +0 -18
- package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js.map +0 -7
- package/src/modules/ai_assistant/lib/__tests__/codemode-tool-annotations.test.ts +0 -58
- package/src/modules/ai_assistant/lib/__tests__/mcp-server-tool-annotations.test.ts +0 -120
- package/src/modules/ai_assistant/lib/__tests__/mcp-tool-annotations.test.ts +0 -57
- package/src/modules/ai_assistant/lib/mcp-tool-annotations.ts +0 -35
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:ai-assistant] found
|
|
1
|
+
[build:ai-assistant] found 232 entry points
|
|
2
2
|
[build:ai-assistant] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -166,7 +166,7 @@ const listPeopleTool = defineAiTool({
|
|
|
166
166
|
MUST rules:
|
|
167
167
|
- MUST set `requiredFeatures` for any tool that reads or writes tenant data. The wildcard-aware ACL matcher is applied before the handler runs.
|
|
168
168
|
- MUST use Zod for `inputSchema` — never raw JSON Schema.
|
|
169
|
-
- MUST set `isMutation: true` on write tools.
|
|
169
|
+
- MUST set `isMutation: true` on write tools. The policy gate strips these from read-only agents and from read-only tenant overrides.
|
|
170
170
|
- MUST route every mutation tool through `prepareMutation(...)` (see the Mutation Approvals guide at `/framework/ai-assistant/mutation-approvals`). Writing directly inside the handler bypasses the approval gate — the runtime fails closed and refuses to return a result to the operator.
|
|
171
171
|
- Mutation preview resolvers SHOULD return a normalized `after` snapshot and display hints when tool inputs contain dictionary IDs or other opaque values. Use `display.fieldLabels`, `display.before`, and `display.after` so `field-diff-card` shows operator-friendly names while `field`, `before`, and `after` keep the raw execution values.
|
|
172
172
|
- MUST expose tools to an agent by listing the tool name in the agent's `allowedTools`. Tools not on the whitelist never reach the model.
|
|
@@ -1,49 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
canReadSearchEntity,
|
|
4
|
-
filterSearchResultsByEntityAccess,
|
|
5
|
-
resolveReadableEntityTypes
|
|
6
|
-
} from "@open-mercato/shared/lib/search/entityAccess";
|
|
7
2
|
import { defineAiTool } from "../lib/ai-tool-definition.js";
|
|
8
|
-
class SearchToolAuthorizationError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "SearchToolAuthorizationError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function resolveSearchIndexer(ctx) {
|
|
15
|
-
try {
|
|
16
|
-
const indexer = ctx.container.resolve("searchIndexer");
|
|
17
|
-
if (indexer && typeof indexer.getEntityConfig === "function" && typeof indexer.getAllEntityConfigs === "function") {
|
|
18
|
-
return indexer;
|
|
19
|
-
}
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
throw new SearchToolAuthorizationError("[internal] Search entity registry unavailable");
|
|
23
|
-
}
|
|
24
|
-
function authorizeEntityAccess(entityType, lookup, subject) {
|
|
25
|
-
if (subject.isSuperAdmin) return;
|
|
26
|
-
let denial;
|
|
27
|
-
const allowed = canReadSearchEntity(entityType, lookup, subject, {
|
|
28
|
-
onDeny: (_, reason) => {
|
|
29
|
-
denial = reason;
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
if (allowed) return;
|
|
33
|
-
if (denial === "unconfigured") {
|
|
34
|
-
throw new SearchToolAuthorizationError(`[internal] Entity type "${entityType}" is not configured for search`);
|
|
35
|
-
}
|
|
36
|
-
if (denial === "no-acl-features") {
|
|
37
|
-
throw new SearchToolAuthorizationError(
|
|
38
|
-
`[internal] Entity type "${entityType}" does not declare aclFeatures; access denied`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
const config = lookup.getEntityConfig(entityType);
|
|
42
|
-
const required = config?.aclFeatures ?? [];
|
|
43
|
-
throw new SearchToolAuthorizationError(
|
|
44
|
-
`[internal] Insufficient permissions for entity "${entityType}". Required: ${required.join(", ")}`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
3
|
const hybridSearchInput = z.object({
|
|
48
4
|
q: z.string().min(1).describe("Search query text."),
|
|
49
5
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum results (default 20, max 100)."),
|
|
@@ -65,49 +21,13 @@ const hybridSearchTool = defineAiTool({
|
|
|
65
21
|
const service = ctx.container.resolve("searchService");
|
|
66
22
|
const limit = input.limit ?? 20;
|
|
67
23
|
const started = Date.now();
|
|
68
|
-
const
|
|
69
|
-
grantedFeatures: ctx.userFeatures,
|
|
70
|
-
isSuperAdmin: ctx.isSuperAdmin
|
|
71
|
-
};
|
|
72
|
-
if (ctx.isSuperAdmin) {
|
|
73
|
-
const results2 = await service.search(input.q, {
|
|
74
|
-
tenantId: ctx.tenantId,
|
|
75
|
-
organizationId: ctx.organizationId,
|
|
76
|
-
limit,
|
|
77
|
-
strategies: input.strategies,
|
|
78
|
-
entityTypes: input.entityTypes
|
|
79
|
-
});
|
|
80
|
-
const timingMs2 = Date.now() - started;
|
|
81
|
-
const strategiesUsed2 = Array.from(
|
|
82
|
-
new Set(results2.map((result) => result.source).filter((id) => typeof id === "string"))
|
|
83
|
-
);
|
|
84
|
-
return {
|
|
85
|
-
query: input.q,
|
|
86
|
-
totalResults: results2.length,
|
|
87
|
-
results: results2,
|
|
88
|
-
strategiesUsed: strategiesUsed2,
|
|
89
|
-
timing: { ms: timingMs2 }
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
const lookup = resolveSearchIndexer(ctx);
|
|
93
|
-
const readableEntityTypes = resolveReadableEntityTypes(lookup, subject, input.entityTypes);
|
|
94
|
-
if (readableEntityTypes && readableEntityTypes.length === 0) {
|
|
95
|
-
return {
|
|
96
|
-
query: input.q,
|
|
97
|
-
totalResults: 0,
|
|
98
|
-
results: [],
|
|
99
|
-
strategiesUsed: [],
|
|
100
|
-
timing: { ms: Date.now() - started }
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
const rawResults = await service.search(input.q, {
|
|
24
|
+
const results = await service.search(input.q, {
|
|
104
25
|
tenantId: ctx.tenantId,
|
|
105
26
|
organizationId: ctx.organizationId,
|
|
106
27
|
limit,
|
|
107
28
|
strategies: input.strategies,
|
|
108
|
-
entityTypes:
|
|
29
|
+
entityTypes: input.entityTypes
|
|
109
30
|
});
|
|
110
|
-
const results = filterSearchResultsByEntityAccess(rawResults, lookup, subject);
|
|
111
31
|
const timingMs = Date.now() - started;
|
|
112
32
|
const strategiesUsed = Array.from(
|
|
113
33
|
new Set(results.map((result) => result.source).filter((id) => typeof id === "string"))
|
|
@@ -137,23 +57,13 @@ const getRecordContextTool = defineAiTool({
|
|
|
137
57
|
throw new Error("Tenant context is required for search.get_record_context");
|
|
138
58
|
}
|
|
139
59
|
const input = getRecordContextInput.parse(rawInput);
|
|
140
|
-
const subject = {
|
|
141
|
-
grantedFeatures: ctx.userFeatures,
|
|
142
|
-
isSuperAdmin: ctx.isSuperAdmin
|
|
143
|
-
};
|
|
144
|
-
let lookup;
|
|
145
|
-
if (!ctx.isSuperAdmin) {
|
|
146
|
-
lookup = resolveSearchIndexer(ctx);
|
|
147
|
-
authorizeEntityAccess(input.entityId, lookup, subject);
|
|
148
|
-
}
|
|
149
60
|
const service = ctx.container.resolve("searchService");
|
|
150
|
-
const
|
|
61
|
+
const results = await service.search(input.recordId, {
|
|
151
62
|
tenantId: ctx.tenantId,
|
|
152
63
|
organizationId: ctx.organizationId,
|
|
153
64
|
limit: 5,
|
|
154
65
|
entityTypes: [input.entityId]
|
|
155
66
|
});
|
|
156
|
-
const results = lookup ? filterSearchResultsByEntityAccess(rawResults, lookup, subject) : rawResults;
|
|
157
67
|
const match = results.find((result) => result.recordId === input.recordId);
|
|
158
68
|
if (!match) {
|
|
159
69
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/ai-tools/search-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport {
|
|
5
|
-
"mappings": "AAOA,SAAS,SAAS;AAElB
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition } from '../lib/types'\n\ntype SearchServiceLike = {\n search: (query: string, options: SearchOptions) => Promise<SearchResult[]>\n}\n\nconst hybridSearchInput = z.object({\n q: z.string().min(1).describe('Search query text.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum results (default 20, max 100).'),\n strategies: z\n .array(z.enum(['fulltext', 'vector', 'tokens']))\n .optional()\n .describe('Subset of strategies to run; defaults to the module defaults.'),\n entityTypes: z\n .array(z.string())\n .optional()\n .describe('Filter to specific entity ids (e.g. \"catalog:product\").'),\n})\n\nconst hybridSearchTool = defineAiTool({\n name: 'search.hybrid_search',\n displayName: 'Hybrid search',\n description:\n 'Run a global fulltext + vector + token search across enabled entities for the current tenant/organization.',\n inputSchema: hybridSearchInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.hybrid_search')\n }\n const input = hybridSearchInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const limit = input.limit ?? 20\n const started = Date.now()\n const results = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: input.entityTypes,\n })\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n },\n})\n\nconst getRecordContextInput = z.object({\n entityId: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record primary key (UUID).'),\n})\n\nconst getRecordContextTool = defineAiTool({\n name: 'search.get_record_context',\n displayName: 'Get record context',\n description:\n 'Resolve presenter, links, and URL for a specific record by re-querying the search index. Returns { found: false } when no hit matches the recordId.',\n inputSchema: getRecordContextInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.get_record_context')\n }\n const input = getRecordContextInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const results = await service.search(input.recordId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit: 5,\n entityTypes: [input.entityId],\n })\n const match = results.find((result) => result.recordId === input.recordId)\n if (!match) {\n return {\n found: false as const,\n entityId: input.entityId,\n recordId: input.recordId,\n }\n }\n return {\n found: true as const,\n entityId: match.entityId,\n recordId: match.recordId,\n presenter: match.presenter,\n url: match.url,\n links: match.links,\n metadata: match.metadata,\n source: match.source,\n score: match.score,\n }\n },\n})\n\nexport const searchAiTools: AiToolDefinition<any, any>[] = [hybridSearchTool, getRecordContextTool]\n\nexport default searchAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
|
@@ -77,7 +77,6 @@ function AiModerationFlagsPageClient() {
|
|
|
77
77
|
[t]
|
|
78
78
|
);
|
|
79
79
|
const total = query.data?.total ?? 0;
|
|
80
|
-
const totalIsCapped = query.data?.totalIsCapped === true;
|
|
81
80
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
82
81
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
83
82
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: t("ai_assistant.moderationFlags.title", "Moderation flags") }),
|
|
@@ -126,7 +125,6 @@ function AiModerationFlagsPageClient() {
|
|
|
126
125
|
pageSize: PAGE_SIZE,
|
|
127
126
|
total,
|
|
128
127
|
totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
|
|
129
|
-
totalIsCapped,
|
|
130
128
|
onPageChange: setPage
|
|
131
129
|
}
|
|
132
130
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -378,9 +378,6 @@ function buildEntitySchemas(graph) {
|
|
|
378
378
|
}
|
|
379
379
|
const CODE_MODE_MAX_API_CALLS = 50;
|
|
380
380
|
const CODE_MODE_MAX_MUTATION_CALLS = 20;
|
|
381
|
-
function registerCodeModeTool(tool) {
|
|
382
|
-
registerMcpTool(tool, { moduleId: "codemode" });
|
|
383
|
-
}
|
|
384
381
|
async function loadCodeModeTools() {
|
|
385
382
|
const commonTypes = await generateCommonTypes();
|
|
386
383
|
registerSearchTool();
|
|
@@ -388,10 +385,9 @@ async function loadCodeModeTools() {
|
|
|
388
385
|
return 2;
|
|
389
386
|
}
|
|
390
387
|
function registerSearchTool() {
|
|
391
|
-
|
|
388
|
+
registerMcpTool(
|
|
392
389
|
{
|
|
393
390
|
name: "search",
|
|
394
|
-
isMutation: false,
|
|
395
391
|
description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.
|
|
396
392
|
Globals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.
|
|
397
393
|
Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,
|
|
@@ -451,22 +447,17 @@ Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common
|
|
|
451
447
|
_memoryContext: memoryContext
|
|
452
448
|
};
|
|
453
449
|
}
|
|
454
|
-
}
|
|
450
|
+
},
|
|
451
|
+
{ moduleId: "codemode" }
|
|
455
452
|
);
|
|
456
453
|
}
|
|
457
454
|
function registerExecuteTool(commonTypes) {
|
|
458
455
|
const typesBlock = commonTypes ? `
|
|
459
456
|
|
|
460
457
|
${commonTypes}` : "";
|
|
461
|
-
|
|
458
|
+
registerMcpTool(
|
|
462
459
|
{
|
|
463
460
|
name: "execute",
|
|
464
|
-
// api.request() reaches every documented endpoint, including POST/PUT/DELETE,
|
|
465
|
-
// so the tool is neither read-only nor guaranteed non-destructive. It is
|
|
466
|
-
// intentionally exempt from prepareMutation: arbitrary sandbox code cannot
|
|
467
|
-
// provide the structured before/after preview that approval flow requires.
|
|
468
|
-
isMutation: true,
|
|
469
|
-
isDestructive: true,
|
|
470
461
|
description: `Make API calls. Returns JSON.
|
|
471
462
|
Globals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.
|
|
472
463
|
RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,
|
|
@@ -535,7 +526,8 @@ RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collecti
|
|
|
535
526
|
_memoryContext: memoryContext
|
|
536
527
|
};
|
|
537
528
|
}
|
|
538
|
-
}
|
|
529
|
+
},
|
|
530
|
+
{ moduleId: "codemode" }
|
|
539
531
|
);
|
|
540
532
|
}
|
|
541
533
|
function createApiRequestFn(ctx, onCall) {
|