strapi-plugin-ai-sdk 0.6.7 → 0.6.9
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/dist/_chunks/{App-31DoeBYs.js → App-C_BH5Ir4.js} +49 -4
- package/dist/_chunks/{App-k5doNU8o.mjs → App-DKyCb0BY.mjs} +49 -4
- package/dist/_chunks/{index-C6UZ9D-c.mjs → index-BV9DET_M.mjs} +1 -1
- package/dist/_chunks/{index-tCUlQhF4.js → index-DCEjJ0as.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +80 -23
- package/dist/server/index.mjs +80 -23
- package/dist/server/src/tool-logic/index.d.ts +1 -0
- package/dist/server/src/tool-logic/schema-utils.d.ts +9 -11
- package/dist/server/src/tool-logic/search-content.d.ts +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ const reactRouterDom = require("react-router-dom");
|
|
|
6
6
|
const designSystem = require("@strapi/design-system");
|
|
7
7
|
const react = require("react");
|
|
8
8
|
const styled = require("styled-components");
|
|
9
|
-
const index = require("./index-
|
|
9
|
+
const index = require("./index-DCEjJ0as.js");
|
|
10
10
|
const icons = require("@strapi/icons");
|
|
11
11
|
const Markdown = require("react-markdown");
|
|
12
12
|
const remarkGfm = require("remark-gfm");
|
|
@@ -688,6 +688,21 @@ const ToolCallHeader = styled__default.default.button`
|
|
|
688
688
|
background: #dcdce4;
|
|
689
689
|
}
|
|
690
690
|
`;
|
|
691
|
+
const Spinner = styled__default.default.span`
|
|
692
|
+
display: inline-block;
|
|
693
|
+
width: 12px;
|
|
694
|
+
height: 12px;
|
|
695
|
+
border: 2px solid #a5a5ba;
|
|
696
|
+
border-top-color: #4945ff;
|
|
697
|
+
border-radius: 50%;
|
|
698
|
+
animation: spin 0.8s linear infinite;
|
|
699
|
+
margin-left: auto;
|
|
700
|
+
flex-shrink: 0;
|
|
701
|
+
|
|
702
|
+
@keyframes spin {
|
|
703
|
+
to { transform: rotate(360deg); }
|
|
704
|
+
}
|
|
705
|
+
`;
|
|
691
706
|
const ToolCallContent = styled__default.default.pre`
|
|
692
707
|
margin: 0;
|
|
693
708
|
padding: 8px 12px;
|
|
@@ -739,7 +754,7 @@ function ToolCallDisplay({ toolCall }) {
|
|
|
739
754
|
"Tool: ",
|
|
740
755
|
toolCall.toolName
|
|
741
756
|
] }),
|
|
742
|
-
toolCall.output
|
|
757
|
+
toolCall.output === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(Spinner, {}) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: { marginLeft: "auto", fontWeight: 400, opacity: 0.6 }, children: "completed" })
|
|
743
758
|
] }),
|
|
744
759
|
contentLinks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ContentLinksRow, { children: contentLinks.map((link) => /* @__PURE__ */ jsxRuntime.jsx(ContentLinkChip, { to: link.to, children: link.label }, link.to)) }),
|
|
745
760
|
expanded && /* @__PURE__ */ jsxRuntime.jsx(ToolCallContent, { children: toolCall.output === void 0 ? "Waiting for result..." : JSON.stringify(toolCall.output, null, 2) })
|
|
@@ -846,6 +861,29 @@ const TypingDots = styled__default.default.span`
|
|
|
846
861
|
40% { transform: scale(1); opacity: 1; }
|
|
847
862
|
}
|
|
848
863
|
`;
|
|
864
|
+
const ThinkingIndicator = styled__default.default.div`
|
|
865
|
+
display: flex;
|
|
866
|
+
align-items: center;
|
|
867
|
+
gap: 6px;
|
|
868
|
+
margin-top: 8px;
|
|
869
|
+
padding: 6px 0;
|
|
870
|
+
font-size: 12px;
|
|
871
|
+
color: #666687;
|
|
872
|
+
`;
|
|
873
|
+
const ThinkingSpinner = styled__default.default.span`
|
|
874
|
+
display: inline-block;
|
|
875
|
+
width: 14px;
|
|
876
|
+
height: 14px;
|
|
877
|
+
border: 2px solid #dcdce4;
|
|
878
|
+
border-top-color: #4945ff;
|
|
879
|
+
border-radius: 50%;
|
|
880
|
+
animation: spin 0.8s linear infinite;
|
|
881
|
+
flex-shrink: 0;
|
|
882
|
+
|
|
883
|
+
@keyframes spin {
|
|
884
|
+
to { transform: rotate(360deg); }
|
|
885
|
+
}
|
|
886
|
+
`;
|
|
849
887
|
const EmptyState = styled__default.default.div`
|
|
850
888
|
display: flex;
|
|
851
889
|
flex-direction: column;
|
|
@@ -884,8 +922,11 @@ const MessageList = react.forwardRef(
|
|
|
884
922
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", textColor: "neutral400", children: "AI Chat" }),
|
|
885
923
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral500", children: "Send a message to start the conversation" }) })
|
|
886
924
|
] }),
|
|
887
|
-
messages.map((message) => {
|
|
925
|
+
messages.map((message, index2) => {
|
|
888
926
|
const displayContent = message.role === "assistant" && message.content ? autoLinkContentTypeUids(message.content) : message.content;
|
|
927
|
+
const isLastMessage = index2 === messages.length - 1;
|
|
928
|
+
const hasToolsRunning = message.toolCalls?.some((tc) => tc.output === void 0) ?? false;
|
|
929
|
+
const showThinking = isLoading && isLastMessage && message.role === "assistant" && displayContent && hasToolsRunning;
|
|
889
930
|
return /* @__PURE__ */ jsxRuntime.jsxs(MessageRow, { $isUser: message.role === "user", children: [
|
|
890
931
|
message.role === "assistant" && /* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.Sparkle, {}) }),
|
|
891
932
|
/* @__PURE__ */ jsxRuntime.jsxs(MessageBubble, { $isUser: message.role === "user", children: [
|
|
@@ -897,7 +938,11 @@ const MessageList = react.forwardRef(
|
|
|
897
938
|
/* @__PURE__ */ jsxRuntime.jsx("span", {}),
|
|
898
939
|
/* @__PURE__ */ jsxRuntime.jsx("span", {})
|
|
899
940
|
] }),
|
|
900
|
-
message.toolCalls?.filter((tc) => !HIDDEN_TOOLS.has(tc.toolName)).map((tc) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallDisplay, { toolCall: tc }, tc.toolCallId))
|
|
941
|
+
message.toolCalls?.filter((tc) => !HIDDEN_TOOLS.has(tc.toolName)).map((tc) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallDisplay, { toolCall: tc }, tc.toolCallId)),
|
|
942
|
+
showThinking && /* @__PURE__ */ jsxRuntime.jsxs(ThinkingIndicator, { children: [
|
|
943
|
+
/* @__PURE__ */ jsxRuntime.jsx(ThinkingSpinner, {}),
|
|
944
|
+
"Working on it…"
|
|
945
|
+
] })
|
|
901
946
|
] })
|
|
902
947
|
] }, message.id);
|
|
903
948
|
}),
|
|
@@ -4,7 +4,7 @@ import { Link, useNavigate, Routes, Route } from "react-router-dom";
|
|
|
4
4
|
import { Box, Typography, TextInput, Button, Main, SearchForm, Searchbar, Table, Thead, Tr, Th, Tbody, Td, Flex, Pagination, Modal, Field, Textarea, SingleSelect, SingleSelectOption } from "@strapi/design-system";
|
|
5
5
|
import { useState, useEffect, useCallback, forwardRef, useRef, useMemo } from "react";
|
|
6
6
|
import styled from "styled-components";
|
|
7
|
-
import { P as PLUGIN_ID } from "./index-
|
|
7
|
+
import { P as PLUGIN_ID } from "./index-BV9DET_M.mjs";
|
|
8
8
|
import { Plus, Trash, Sparkle, ArrowLeft, Pencil } from "@strapi/icons";
|
|
9
9
|
import Markdown from "react-markdown";
|
|
10
10
|
import remarkGfm from "remark-gfm";
|
|
@@ -682,6 +682,21 @@ const ToolCallHeader = styled.button`
|
|
|
682
682
|
background: #dcdce4;
|
|
683
683
|
}
|
|
684
684
|
`;
|
|
685
|
+
const Spinner = styled.span`
|
|
686
|
+
display: inline-block;
|
|
687
|
+
width: 12px;
|
|
688
|
+
height: 12px;
|
|
689
|
+
border: 2px solid #a5a5ba;
|
|
690
|
+
border-top-color: #4945ff;
|
|
691
|
+
border-radius: 50%;
|
|
692
|
+
animation: spin 0.8s linear infinite;
|
|
693
|
+
margin-left: auto;
|
|
694
|
+
flex-shrink: 0;
|
|
695
|
+
|
|
696
|
+
@keyframes spin {
|
|
697
|
+
to { transform: rotate(360deg); }
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
685
700
|
const ToolCallContent = styled.pre`
|
|
686
701
|
margin: 0;
|
|
687
702
|
padding: 8px 12px;
|
|
@@ -733,7 +748,7 @@ function ToolCallDisplay({ toolCall }) {
|
|
|
733
748
|
"Tool: ",
|
|
734
749
|
toolCall.toolName
|
|
735
750
|
] }),
|
|
736
|
-
toolCall.output
|
|
751
|
+
toolCall.output === void 0 ? /* @__PURE__ */ jsx(Spinner, {}) : /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", fontWeight: 400, opacity: 0.6 }, children: "completed" })
|
|
737
752
|
] }),
|
|
738
753
|
contentLinks.length > 0 && /* @__PURE__ */ jsx(ContentLinksRow, { children: contentLinks.map((link) => /* @__PURE__ */ jsx(ContentLinkChip, { to: link.to, children: link.label }, link.to)) }),
|
|
739
754
|
expanded && /* @__PURE__ */ jsx(ToolCallContent, { children: toolCall.output === void 0 ? "Waiting for result..." : JSON.stringify(toolCall.output, null, 2) })
|
|
@@ -840,6 +855,29 @@ const TypingDots = styled.span`
|
|
|
840
855
|
40% { transform: scale(1); opacity: 1; }
|
|
841
856
|
}
|
|
842
857
|
`;
|
|
858
|
+
const ThinkingIndicator = styled.div`
|
|
859
|
+
display: flex;
|
|
860
|
+
align-items: center;
|
|
861
|
+
gap: 6px;
|
|
862
|
+
margin-top: 8px;
|
|
863
|
+
padding: 6px 0;
|
|
864
|
+
font-size: 12px;
|
|
865
|
+
color: #666687;
|
|
866
|
+
`;
|
|
867
|
+
const ThinkingSpinner = styled.span`
|
|
868
|
+
display: inline-block;
|
|
869
|
+
width: 14px;
|
|
870
|
+
height: 14px;
|
|
871
|
+
border: 2px solid #dcdce4;
|
|
872
|
+
border-top-color: #4945ff;
|
|
873
|
+
border-radius: 50%;
|
|
874
|
+
animation: spin 0.8s linear infinite;
|
|
875
|
+
flex-shrink: 0;
|
|
876
|
+
|
|
877
|
+
@keyframes spin {
|
|
878
|
+
to { transform: rotate(360deg); }
|
|
879
|
+
}
|
|
880
|
+
`;
|
|
843
881
|
const EmptyState = styled.div`
|
|
844
882
|
display: flex;
|
|
845
883
|
flex-direction: column;
|
|
@@ -878,8 +916,11 @@ const MessageList = forwardRef(
|
|
|
878
916
|
/* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral400", children: "AI Chat" }),
|
|
879
917
|
/* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral500", children: "Send a message to start the conversation" }) })
|
|
880
918
|
] }),
|
|
881
|
-
messages.map((message) => {
|
|
919
|
+
messages.map((message, index) => {
|
|
882
920
|
const displayContent = message.role === "assistant" && message.content ? autoLinkContentTypeUids(message.content) : message.content;
|
|
921
|
+
const isLastMessage = index === messages.length - 1;
|
|
922
|
+
const hasToolsRunning = message.toolCalls?.some((tc) => tc.output === void 0) ?? false;
|
|
923
|
+
const showThinking = isLoading && isLastMessage && message.role === "assistant" && displayContent && hasToolsRunning;
|
|
883
924
|
return /* @__PURE__ */ jsxs(MessageRow, { $isUser: message.role === "user", children: [
|
|
884
925
|
message.role === "assistant" && /* @__PURE__ */ jsx(SparkleIcon, { children: /* @__PURE__ */ jsx(Sparkle, {}) }),
|
|
885
926
|
/* @__PURE__ */ jsxs(MessageBubble, { $isUser: message.role === "user", children: [
|
|
@@ -891,7 +932,11 @@ const MessageList = forwardRef(
|
|
|
891
932
|
/* @__PURE__ */ jsx("span", {}),
|
|
892
933
|
/* @__PURE__ */ jsx("span", {})
|
|
893
934
|
] }),
|
|
894
|
-
message.toolCalls?.filter((tc) => !HIDDEN_TOOLS.has(tc.toolName)).map((tc) => /* @__PURE__ */ jsx(ToolCallDisplay, { toolCall: tc }, tc.toolCallId))
|
|
935
|
+
message.toolCalls?.filter((tc) => !HIDDEN_TOOLS.has(tc.toolName)).map((tc) => /* @__PURE__ */ jsx(ToolCallDisplay, { toolCall: tc }, tc.toolCallId)),
|
|
936
|
+
showThinking && /* @__PURE__ */ jsxs(ThinkingIndicator, { children: [
|
|
937
|
+
/* @__PURE__ */ jsx(ThinkingSpinner, {}),
|
|
938
|
+
"Working on it…"
|
|
939
|
+
] })
|
|
895
940
|
] })
|
|
896
941
|
] }, message.id);
|
|
897
942
|
}),
|
|
@@ -37,7 +37,7 @@ const index = {
|
|
|
37
37
|
defaultMessage: PLUGIN_ID
|
|
38
38
|
},
|
|
39
39
|
Component: async () => {
|
|
40
|
-
const { App } = await Promise.resolve().then(() => require("./App-
|
|
40
|
+
const { App } = await Promise.resolve().then(() => require("./App-C_BH5Ir4.js"));
|
|
41
41
|
return App;
|
|
42
42
|
}
|
|
43
43
|
});
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -297,7 +297,7 @@ const searchContentSchema = zod.z.object({
|
|
|
297
297
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
298
298
|
),
|
|
299
299
|
query: zod.z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
300
|
-
filters: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe('Strapi filter object
|
|
300
|
+
filters: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe('Strapi filter object. Scalar: { title: { $containsi: "hello" } }. Relation: { author: { name: { $eq: "John" } } }. ManyToMany: { contentTags: { title: { $eq: "tutorial" } } }. Operators: $eq, $ne, $containsi, $in, $gt, $lt, $gte, $lte, $null, $notNull.'),
|
|
301
301
|
fields: zod.z.array(zod.z.string()).optional().describe("Specific fields to return. If omitted, returns all fields (large content fields stripped unless includeContent is true)."),
|
|
302
302
|
sort: zod.z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
303
303
|
page: zod.z.number().optional().default(1).describe("Page number (starts at 1)"),
|
|
@@ -307,7 +307,7 @@ const searchContentSchema = zod.z.object({
|
|
|
307
307
|
populate: zod.z.union([zod.z.string(), zod.z.array(zod.z.string()), zod.z.record(zod.z.string(), zod.z.unknown())]).optional().describe('Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'),
|
|
308
308
|
includeContent: zod.z.boolean().optional().default(false).describe("When true, includes large content fields (content, blocks, body, etc.) in results. Default false to reduce context size.")
|
|
309
309
|
});
|
|
310
|
-
const searchContentDescription = 'Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. "createdAt:desc") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content.';
|
|
310
|
+
const searchContentDescription = 'Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. "createdAt:desc") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content. To filter by a relation, nest the filter: { relationField: { fieldOnRelation: { $operator: "value" } } }.';
|
|
311
311
|
function stripLargeFields(obj) {
|
|
312
312
|
const stripped = {};
|
|
313
313
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -671,24 +671,33 @@ function getDisplayField(strapi, contentType) {
|
|
|
671
671
|
}
|
|
672
672
|
return "id";
|
|
673
673
|
}
|
|
674
|
+
const MANY_RELATIONS = /* @__PURE__ */ new Set(["oneToMany", "manyToMany"]);
|
|
674
675
|
function resolveFieldPath(strapi, contentType, fieldPath) {
|
|
675
676
|
const parts = fieldPath.split(".");
|
|
676
677
|
const topField = parts[0];
|
|
677
678
|
const attr = getAttribute(strapi, contentType, topField);
|
|
678
679
|
if (!attr) {
|
|
679
|
-
return { resolvedPath: fieldPath, populate: [] };
|
|
680
|
+
return { resolvedPath: fieldPath, populate: [], isArray: false };
|
|
680
681
|
}
|
|
681
682
|
if (attr.type === "relation" && attr.target) {
|
|
683
|
+
const isArray = MANY_RELATIONS.has(attr.relation ?? "");
|
|
682
684
|
if (parts.length === 1) {
|
|
683
685
|
const displayField = getDisplayField(strapi, attr.target);
|
|
684
686
|
return {
|
|
685
687
|
resolvedPath: `${topField}.${displayField}`,
|
|
686
|
-
populate: [topField]
|
|
688
|
+
populate: [topField],
|
|
689
|
+
isArray,
|
|
690
|
+
selectField: displayField
|
|
687
691
|
};
|
|
688
692
|
}
|
|
689
|
-
return {
|
|
693
|
+
return {
|
|
694
|
+
resolvedPath: fieldPath,
|
|
695
|
+
populate: [topField],
|
|
696
|
+
isArray,
|
|
697
|
+
selectField: parts.slice(1).join(".")
|
|
698
|
+
};
|
|
690
699
|
}
|
|
691
|
-
return { resolvedPath: fieldPath, populate: [] };
|
|
700
|
+
return { resolvedPath: fieldPath, populate: [], isArray: false };
|
|
692
701
|
}
|
|
693
702
|
const MAX_PAGINATE = 1e3;
|
|
694
703
|
const PAGE_SIZE = 100;
|
|
@@ -697,7 +706,7 @@ const aggregateContentSchema = zod.z.object({
|
|
|
697
706
|
operation: zod.z.enum(["count", "countByField", "countByDateRange"]).describe(
|
|
698
707
|
"count — total count; countByField — group by a field; countByDateRange — bucket by date"
|
|
699
708
|
),
|
|
700
|
-
filters: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe('Strapi filter object
|
|
709
|
+
filters: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe('Strapi filter object. Scalar: { title: { $containsi: "hello" } }. Relation: { author: { name: { $eq: "John" } } }. ManyToMany: { contentTags: { title: { $eq: "tutorial" } } }.'),
|
|
701
710
|
groupByField: zod.z.string().optional().describe('Field to group by for countByField. Just use the field name — relation fields are auto-resolved to their display name (e.g. "author", "category"). You can also use dot paths like "author.email" for a specific sub-field.'),
|
|
702
711
|
dateField: zod.z.string().optional().default("createdAt").describe('Date field for countByDateRange (default: "createdAt")'),
|
|
703
712
|
granularity: zod.z.enum(["day", "week", "month"]).optional().default("month").describe("Bucket granularity for countByDateRange"),
|
|
@@ -727,17 +736,30 @@ function resolveField(doc, fieldPath) {
|
|
|
727
736
|
let current = doc;
|
|
728
737
|
for (const part of parts) {
|
|
729
738
|
if (current == null || typeof current !== "object") return void 0;
|
|
739
|
+
if (Array.isArray(current)) return void 0;
|
|
730
740
|
current = current[part];
|
|
731
741
|
}
|
|
732
742
|
return current;
|
|
733
743
|
}
|
|
734
|
-
|
|
744
|
+
function buildPopulate(resolved) {
|
|
745
|
+
if (resolved.populate.length === 0) return "*";
|
|
746
|
+
const populate = {};
|
|
747
|
+
for (const rel of resolved.populate) {
|
|
748
|
+
if (resolved.selectField) {
|
|
749
|
+
populate[rel] = { fields: [resolved.selectField] };
|
|
750
|
+
} else {
|
|
751
|
+
populate[rel] = true;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return populate;
|
|
755
|
+
}
|
|
756
|
+
async function paginateAll(strapi, contentType, baseQuery, populate = "*") {
|
|
735
757
|
const docs = [];
|
|
736
758
|
let page = 1;
|
|
737
759
|
while (docs.length < MAX_PAGINATE) {
|
|
738
760
|
const results = await strapi.documents(contentType).findMany({
|
|
739
761
|
...baseQuery,
|
|
740
|
-
populate
|
|
762
|
+
populate,
|
|
741
763
|
page,
|
|
742
764
|
pageSize: PAGE_SIZE
|
|
743
765
|
});
|
|
@@ -752,22 +774,38 @@ const DISPLAY_CANDIDATES = ["name", "title", "username", "label", "slug", "email
|
|
|
752
774
|
function toDisplayValue(raw) {
|
|
753
775
|
if (raw == null) return "(empty)";
|
|
754
776
|
if (typeof raw !== "object") return String(raw);
|
|
755
|
-
if (Array.isArray(raw)) {
|
|
756
|
-
if (raw.length === 0) return "(empty)";
|
|
757
|
-
return raw.map((item) => toDisplayValue(item)).join(", ");
|
|
758
|
-
}
|
|
759
777
|
const obj = raw;
|
|
760
778
|
for (const key of DISPLAY_CANDIDATES) {
|
|
761
779
|
if (obj[key] != null) return String(obj[key]);
|
|
762
780
|
}
|
|
763
781
|
return obj.id != null ? String(obj.id) : "(empty)";
|
|
764
782
|
}
|
|
765
|
-
function groupDocs(docs,
|
|
783
|
+
function groupDocs(docs, resolved) {
|
|
766
784
|
const counts = /* @__PURE__ */ new Map();
|
|
785
|
+
const topField = resolved.resolvedPath.split(".")[0];
|
|
786
|
+
const subPath = resolved.resolvedPath.split(".").slice(1).join(".");
|
|
767
787
|
for (const doc of docs) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
788
|
+
if (resolved.isArray) {
|
|
789
|
+
const items = doc[topField];
|
|
790
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
791
|
+
counts.set("(empty)", (counts.get("(empty)") ?? 0) + 1);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
for (const item of items) {
|
|
795
|
+
let value;
|
|
796
|
+
if (subPath && typeof item === "object" && item != null) {
|
|
797
|
+
const sub = resolveField(item, subPath);
|
|
798
|
+
value = sub != null ? String(sub) : toDisplayValue(item);
|
|
799
|
+
} else {
|
|
800
|
+
value = toDisplayValue(item);
|
|
801
|
+
}
|
|
802
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
const raw = resolveField(doc, resolved.resolvedPath);
|
|
806
|
+
const value = toDisplayValue(raw);
|
|
807
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
808
|
+
}
|
|
771
809
|
}
|
|
772
810
|
return Array.from(counts.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count);
|
|
773
811
|
}
|
|
@@ -806,10 +844,11 @@ async function aggregateContent(strapi, params) {
|
|
|
806
844
|
if (!groupByField) {
|
|
807
845
|
throw new Error("groupByField is required for countByField operation");
|
|
808
846
|
}
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
|
|
847
|
+
const resolved = resolveFieldPath(strapi, contentType, groupByField);
|
|
848
|
+
const populate = buildPopulate(resolved);
|
|
849
|
+
const docs = await paginateAll(strapi, contentType, baseQuery, populate);
|
|
850
|
+
const groups = groupDocs(docs, resolved);
|
|
851
|
+
return { total: docs.length, groups, resolvedField: resolved.resolvedPath };
|
|
813
852
|
}
|
|
814
853
|
case "countByDateRange": {
|
|
815
854
|
const { dateField = "createdAt", granularity = "month" } = params;
|
|
@@ -2154,8 +2193,26 @@ function describeTools(tools) {
|
|
|
2154
2193
|
return `Available tools:
|
|
2155
2194
|
${lines.join("\n")}`;
|
|
2156
2195
|
}
|
|
2157
|
-
const DEFAULT_PREAMBLE =
|
|
2158
|
-
|
|
2196
|
+
const DEFAULT_PREAMBLE = `You are a Strapi CMS assistant. Use your tools to fulfill user requests. When asked to create or update content, use the appropriate tool — do not tell the user you cannot. When performing bulk operations (e.g. publish multiple items), call multiple tools in parallel in a single step rather than one at a time.
|
|
2197
|
+
|
|
2198
|
+
For analytics and counting questions (e.g. "how many", "count", "distribution", "trends", "breakdown"), use the aggregateContent tool instead of searchContent — it is faster and purpose-built for these queries. Present analytics results as markdown tables. After showing analytics results, suggest 2-3 follow-up questions the user might find useful under a "**You might also want to know:**" heading.
|
|
2199
|
+
|
|
2200
|
+
Strapi filter syntax for searchContent and aggregateContent:
|
|
2201
|
+
- Scalar fields: { title: { $containsi: "hello" } }
|
|
2202
|
+
- Relation (manyToOne): { author: { name: { $eq: "John" } } }
|
|
2203
|
+
- Relation (manyToMany): { contentTags: { title: { $eq: "tutorial" } } }
|
|
2204
|
+
- Always nest relation filters as: { relationField: { fieldOnRelatedType: { $operator: value } } }
|
|
2205
|
+
- Never use flat dot-path syntax like "contentTags.title" in filters — always use nested objects.`;
|
|
2206
|
+
const DEFAULT_PUBLIC_PREAMBLE = `You are a helpful public assistant for this website. Use your tools to answer questions about the site content. You cannot modify any content or perform administrative actions.
|
|
2207
|
+
|
|
2208
|
+
For analytics and counting questions (e.g. "how many", "count", "distribution", "trends", "breakdown"), use the aggregateContent tool instead of searchContent — it is faster and purpose-built for these queries. Present analytics results as markdown tables. After showing analytics results, suggest 2-3 follow-up questions the user might find useful under a "**You might also want to know:**" heading.
|
|
2209
|
+
|
|
2210
|
+
Strapi filter syntax for searchContent and aggregateContent:
|
|
2211
|
+
- Scalar fields: { title: { $containsi: "hello" } }
|
|
2212
|
+
- Relation (manyToOne): { author: { name: { $eq: "John" } } }
|
|
2213
|
+
- Relation (manyToMany): { contentTags: { title: { $eq: "tutorial" } } }
|
|
2214
|
+
- Always nest relation filters as: { relationField: { fieldOnRelatedType: { $operator: value } } }
|
|
2215
|
+
- Never use flat dot-path syntax like "contentTags.title" in filters — always use nested objects.`;
|
|
2159
2216
|
function composeSystemPrompt(config2, toolsDescription, override) {
|
|
2160
2217
|
const base = override || config2?.systemPrompt || DEFAULT_PREAMBLE;
|
|
2161
2218
|
if (base.includes("{tools}")) {
|
package/dist/server/index.mjs
CHANGED
|
@@ -277,7 +277,7 @@ const searchContentSchema = z.object({
|
|
|
277
277
|
'The content type UID to search, e.g. "api::article.article" or "plugin::users-permissions.user"'
|
|
278
278
|
),
|
|
279
279
|
query: z.string().optional().describe("Full-text search query string (searches across all searchable text fields)"),
|
|
280
|
-
filters: z.record(z.string(), z.unknown()).optional().describe('Strapi filter object
|
|
280
|
+
filters: z.record(z.string(), z.unknown()).optional().describe('Strapi filter object. Scalar: { title: { $containsi: "hello" } }. Relation: { author: { name: { $eq: "John" } } }. ManyToMany: { contentTags: { title: { $eq: "tutorial" } } }. Operators: $eq, $ne, $containsi, $in, $gt, $lt, $gte, $lte, $null, $notNull.'),
|
|
281
281
|
fields: z.array(z.string()).optional().describe("Specific fields to return. If omitted, returns all fields (large content fields stripped unless includeContent is true)."),
|
|
282
282
|
sort: z.string().optional().describe('Sort order, e.g. "createdAt:desc"'),
|
|
283
283
|
page: z.number().optional().default(1).describe("Page number (starts at 1)"),
|
|
@@ -287,7 +287,7 @@ const searchContentSchema = z.object({
|
|
|
287
287
|
populate: z.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())]).optional().describe('Relations to populate. Defaults to "*" (all). Can be a string, array, or object.'),
|
|
288
288
|
includeContent: z.boolean().optional().default(false).describe("When true, includes large content fields (content, blocks, body, etc.) in results. Default false to reduce context size.")
|
|
289
289
|
});
|
|
290
|
-
const searchContentDescription = 'Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. "createdAt:desc") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content.';
|
|
290
|
+
const searchContentDescription = 'Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. "createdAt:desc") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results — set includeContent to true or use fields to get full content. To filter by a relation, nest the filter: { relationField: { fieldOnRelation: { $operator: "value" } } }.';
|
|
291
291
|
function stripLargeFields(obj) {
|
|
292
292
|
const stripped = {};
|
|
293
293
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -651,24 +651,33 @@ function getDisplayField(strapi, contentType) {
|
|
|
651
651
|
}
|
|
652
652
|
return "id";
|
|
653
653
|
}
|
|
654
|
+
const MANY_RELATIONS = /* @__PURE__ */ new Set(["oneToMany", "manyToMany"]);
|
|
654
655
|
function resolveFieldPath(strapi, contentType, fieldPath) {
|
|
655
656
|
const parts = fieldPath.split(".");
|
|
656
657
|
const topField = parts[0];
|
|
657
658
|
const attr = getAttribute(strapi, contentType, topField);
|
|
658
659
|
if (!attr) {
|
|
659
|
-
return { resolvedPath: fieldPath, populate: [] };
|
|
660
|
+
return { resolvedPath: fieldPath, populate: [], isArray: false };
|
|
660
661
|
}
|
|
661
662
|
if (attr.type === "relation" && attr.target) {
|
|
663
|
+
const isArray = MANY_RELATIONS.has(attr.relation ?? "");
|
|
662
664
|
if (parts.length === 1) {
|
|
663
665
|
const displayField = getDisplayField(strapi, attr.target);
|
|
664
666
|
return {
|
|
665
667
|
resolvedPath: `${topField}.${displayField}`,
|
|
666
|
-
populate: [topField]
|
|
668
|
+
populate: [topField],
|
|
669
|
+
isArray,
|
|
670
|
+
selectField: displayField
|
|
667
671
|
};
|
|
668
672
|
}
|
|
669
|
-
return {
|
|
673
|
+
return {
|
|
674
|
+
resolvedPath: fieldPath,
|
|
675
|
+
populate: [topField],
|
|
676
|
+
isArray,
|
|
677
|
+
selectField: parts.slice(1).join(".")
|
|
678
|
+
};
|
|
670
679
|
}
|
|
671
|
-
return { resolvedPath: fieldPath, populate: [] };
|
|
680
|
+
return { resolvedPath: fieldPath, populate: [], isArray: false };
|
|
672
681
|
}
|
|
673
682
|
const MAX_PAGINATE = 1e3;
|
|
674
683
|
const PAGE_SIZE = 100;
|
|
@@ -677,7 +686,7 @@ const aggregateContentSchema = z.object({
|
|
|
677
686
|
operation: z.enum(["count", "countByField", "countByDateRange"]).describe(
|
|
678
687
|
"count — total count; countByField — group by a field; countByDateRange — bucket by date"
|
|
679
688
|
),
|
|
680
|
-
filters: z.record(z.string(), z.unknown()).optional().describe('Strapi filter object
|
|
689
|
+
filters: z.record(z.string(), z.unknown()).optional().describe('Strapi filter object. Scalar: { title: { $containsi: "hello" } }. Relation: { author: { name: { $eq: "John" } } }. ManyToMany: { contentTags: { title: { $eq: "tutorial" } } }.'),
|
|
681
690
|
groupByField: z.string().optional().describe('Field to group by for countByField. Just use the field name — relation fields are auto-resolved to their display name (e.g. "author", "category"). You can also use dot paths like "author.email" for a specific sub-field.'),
|
|
682
691
|
dateField: z.string().optional().default("createdAt").describe('Date field for countByDateRange (default: "createdAt")'),
|
|
683
692
|
granularity: z.enum(["day", "week", "month"]).optional().default("month").describe("Bucket granularity for countByDateRange"),
|
|
@@ -707,17 +716,30 @@ function resolveField(doc, fieldPath) {
|
|
|
707
716
|
let current = doc;
|
|
708
717
|
for (const part of parts) {
|
|
709
718
|
if (current == null || typeof current !== "object") return void 0;
|
|
719
|
+
if (Array.isArray(current)) return void 0;
|
|
710
720
|
current = current[part];
|
|
711
721
|
}
|
|
712
722
|
return current;
|
|
713
723
|
}
|
|
714
|
-
|
|
724
|
+
function buildPopulate(resolved) {
|
|
725
|
+
if (resolved.populate.length === 0) return "*";
|
|
726
|
+
const populate = {};
|
|
727
|
+
for (const rel of resolved.populate) {
|
|
728
|
+
if (resolved.selectField) {
|
|
729
|
+
populate[rel] = { fields: [resolved.selectField] };
|
|
730
|
+
} else {
|
|
731
|
+
populate[rel] = true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return populate;
|
|
735
|
+
}
|
|
736
|
+
async function paginateAll(strapi, contentType, baseQuery, populate = "*") {
|
|
715
737
|
const docs = [];
|
|
716
738
|
let page = 1;
|
|
717
739
|
while (docs.length < MAX_PAGINATE) {
|
|
718
740
|
const results = await strapi.documents(contentType).findMany({
|
|
719
741
|
...baseQuery,
|
|
720
|
-
populate
|
|
742
|
+
populate,
|
|
721
743
|
page,
|
|
722
744
|
pageSize: PAGE_SIZE
|
|
723
745
|
});
|
|
@@ -732,22 +754,38 @@ const DISPLAY_CANDIDATES = ["name", "title", "username", "label", "slug", "email
|
|
|
732
754
|
function toDisplayValue(raw) {
|
|
733
755
|
if (raw == null) return "(empty)";
|
|
734
756
|
if (typeof raw !== "object") return String(raw);
|
|
735
|
-
if (Array.isArray(raw)) {
|
|
736
|
-
if (raw.length === 0) return "(empty)";
|
|
737
|
-
return raw.map((item) => toDisplayValue(item)).join(", ");
|
|
738
|
-
}
|
|
739
757
|
const obj = raw;
|
|
740
758
|
for (const key of DISPLAY_CANDIDATES) {
|
|
741
759
|
if (obj[key] != null) return String(obj[key]);
|
|
742
760
|
}
|
|
743
761
|
return obj.id != null ? String(obj.id) : "(empty)";
|
|
744
762
|
}
|
|
745
|
-
function groupDocs(docs,
|
|
763
|
+
function groupDocs(docs, resolved) {
|
|
746
764
|
const counts = /* @__PURE__ */ new Map();
|
|
765
|
+
const topField = resolved.resolvedPath.split(".")[0];
|
|
766
|
+
const subPath = resolved.resolvedPath.split(".").slice(1).join(".");
|
|
747
767
|
for (const doc of docs) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
768
|
+
if (resolved.isArray) {
|
|
769
|
+
const items = doc[topField];
|
|
770
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
771
|
+
counts.set("(empty)", (counts.get("(empty)") ?? 0) + 1);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
for (const item of items) {
|
|
775
|
+
let value;
|
|
776
|
+
if (subPath && typeof item === "object" && item != null) {
|
|
777
|
+
const sub = resolveField(item, subPath);
|
|
778
|
+
value = sub != null ? String(sub) : toDisplayValue(item);
|
|
779
|
+
} else {
|
|
780
|
+
value = toDisplayValue(item);
|
|
781
|
+
}
|
|
782
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
const raw = resolveField(doc, resolved.resolvedPath);
|
|
786
|
+
const value = toDisplayValue(raw);
|
|
787
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
788
|
+
}
|
|
751
789
|
}
|
|
752
790
|
return Array.from(counts.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count);
|
|
753
791
|
}
|
|
@@ -786,10 +824,11 @@ async function aggregateContent(strapi, params) {
|
|
|
786
824
|
if (!groupByField) {
|
|
787
825
|
throw new Error("groupByField is required for countByField operation");
|
|
788
826
|
}
|
|
789
|
-
const
|
|
790
|
-
const
|
|
791
|
-
const
|
|
792
|
-
|
|
827
|
+
const resolved = resolveFieldPath(strapi, contentType, groupByField);
|
|
828
|
+
const populate = buildPopulate(resolved);
|
|
829
|
+
const docs = await paginateAll(strapi, contentType, baseQuery, populate);
|
|
830
|
+
const groups = groupDocs(docs, resolved);
|
|
831
|
+
return { total: docs.length, groups, resolvedField: resolved.resolvedPath };
|
|
793
832
|
}
|
|
794
833
|
case "countByDateRange": {
|
|
795
834
|
const { dateField = "createdAt", granularity = "month" } = params;
|
|
@@ -2134,8 +2173,26 @@ function describeTools(tools) {
|
|
|
2134
2173
|
return `Available tools:
|
|
2135
2174
|
${lines.join("\n")}`;
|
|
2136
2175
|
}
|
|
2137
|
-
const DEFAULT_PREAMBLE =
|
|
2138
|
-
|
|
2176
|
+
const DEFAULT_PREAMBLE = `You are a Strapi CMS assistant. Use your tools to fulfill user requests. When asked to create or update content, use the appropriate tool — do not tell the user you cannot. When performing bulk operations (e.g. publish multiple items), call multiple tools in parallel in a single step rather than one at a time.
|
|
2177
|
+
|
|
2178
|
+
For analytics and counting questions (e.g. "how many", "count", "distribution", "trends", "breakdown"), use the aggregateContent tool instead of searchContent — it is faster and purpose-built for these queries. Present analytics results as markdown tables. After showing analytics results, suggest 2-3 follow-up questions the user might find useful under a "**You might also want to know:**" heading.
|
|
2179
|
+
|
|
2180
|
+
Strapi filter syntax for searchContent and aggregateContent:
|
|
2181
|
+
- Scalar fields: { title: { $containsi: "hello" } }
|
|
2182
|
+
- Relation (manyToOne): { author: { name: { $eq: "John" } } }
|
|
2183
|
+
- Relation (manyToMany): { contentTags: { title: { $eq: "tutorial" } } }
|
|
2184
|
+
- Always nest relation filters as: { relationField: { fieldOnRelatedType: { $operator: value } } }
|
|
2185
|
+
- Never use flat dot-path syntax like "contentTags.title" in filters — always use nested objects.`;
|
|
2186
|
+
const DEFAULT_PUBLIC_PREAMBLE = `You are a helpful public assistant for this website. Use your tools to answer questions about the site content. You cannot modify any content or perform administrative actions.
|
|
2187
|
+
|
|
2188
|
+
For analytics and counting questions (e.g. "how many", "count", "distribution", "trends", "breakdown"), use the aggregateContent tool instead of searchContent — it is faster and purpose-built for these queries. Present analytics results as markdown tables. After showing analytics results, suggest 2-3 follow-up questions the user might find useful under a "**You might also want to know:**" heading.
|
|
2189
|
+
|
|
2190
|
+
Strapi filter syntax for searchContent and aggregateContent:
|
|
2191
|
+
- Scalar fields: { title: { $containsi: "hello" } }
|
|
2192
|
+
- Relation (manyToOne): { author: { name: { $eq: "John" } } }
|
|
2193
|
+
- Relation (manyToMany): { contentTags: { title: { $eq: "tutorial" } } }
|
|
2194
|
+
- Always nest relation filters as: { relationField: { fieldOnRelatedType: { $operator: value } } }
|
|
2195
|
+
- Never use flat dot-path syntax like "contentTags.title" in filters — always use nested objects.`;
|
|
2139
2196
|
function composeSystemPrompt(config2, toolsDescription, override) {
|
|
2140
2197
|
const base = override || config2?.systemPrompt || DEFAULT_PREAMBLE;
|
|
2141
2198
|
if (base.includes("{tools}")) {
|
|
@@ -19,3 +19,4 @@ export type { RecallPublicMemoriesParams, RecallPublicMemoriesResult } from './r
|
|
|
19
19
|
export { aggregateContent, aggregateContentSchema, aggregateContentDescription } from './aggregate-content';
|
|
20
20
|
export type { AggregateContentParams, AggregateContentResult } from './aggregate-content';
|
|
21
21
|
export { resolveFieldPath, getDisplayField, isRelation, getRelationTarget, getSchema } from './schema-utils';
|
|
22
|
+
export type { ResolvedField } from './schema-utils';
|
|
@@ -35,17 +35,15 @@ export declare function getRelationTarget(strapi: Core.Strapi, contentType: stri
|
|
|
35
35
|
* then falls back to the first string/text field, then 'id'.
|
|
36
36
|
*/
|
|
37
37
|
export declare function getDisplayField(strapi: Core.Strapi, contentType: string): string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* resolve it to the actual dot-path needed to extract a display value from a populated document.
|
|
41
|
-
*
|
|
42
|
-
* If the field is a relation with no sub-field specified, appends the best display field
|
|
43
|
-
* from the target schema (e.g. "author" → "author.name").
|
|
44
|
-
*
|
|
45
|
-
* Returns the resolved path and any top-level relations that need populating.
|
|
46
|
-
*/
|
|
47
|
-
export declare function resolveFieldPath(strapi: Core.Strapi, contentType: string, fieldPath: string): {
|
|
38
|
+
export interface ResolvedField {
|
|
39
|
+
/** The dot-path to extract the display value (e.g. "author.name") */
|
|
48
40
|
resolvedPath: string;
|
|
41
|
+
/** Top-level relations that must be populated */
|
|
49
42
|
populate: string[];
|
|
50
|
-
|
|
43
|
+
/** True if the relation is a *ToMany (value is an array of items) */
|
|
44
|
+
isArray: boolean;
|
|
45
|
+
/** The sub-field to select from the relation (for targeted populate) */
|
|
46
|
+
selectField?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function resolveFieldPath(strapi: Core.Strapi, contentType: string, fieldPath: string): ResolvedField;
|
|
51
49
|
export {};
|
|
@@ -16,7 +16,7 @@ export declare const searchContentSchema: z.ZodObject<{
|
|
|
16
16
|
populate: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
|
|
17
17
|
includeContent: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
18
18
|
}, z.core.$strip>;
|
|
19
|
-
export declare const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. \"createdAt:desc\") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results \u2014 set includeContent to true or use fields to get full content.";
|
|
19
|
+
export declare const searchContentDescription = "Search and query any Strapi content type. Use listContentTypes first to discover available content types and their fields, then use this tool to query specific collections. Use sort (e.g. \"createdAt:desc\") and pageSize: 1 to get the latest entry. By default, large content fields are stripped from results \u2014 set includeContent to true or use fields to get full content. To filter by a relation, nest the filter: { relationField: { fieldOnRelation: { $operator: \"value\" } } }.";
|
|
20
20
|
export interface SearchContentParams {
|
|
21
21
|
contentType: string;
|
|
22
22
|
query?: string;
|
package/package.json
CHANGED