@agent-native/core 0.119.2 → 0.119.4
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/package.json +3 -3
- package/corpus/core/src/agent/production-agent.ts +107 -125
- package/corpus/core/src/agent/run-manager.ts +45 -2
- package/corpus/core/src/agent/thread-data-builder.ts +85 -8
- package/corpus/core/src/cli/create.ts +44 -5
- package/corpus/core/src/client/AssistantChat.tsx +37 -9
- package/corpus/core/src/client/chat/message-components.tsx +9 -1
- package/corpus/core/src/client/chat/repo-helpers.ts +77 -0
- package/corpus/core/src/client/chat/tool-call-display.tsx +51 -15
- package/corpus/core/src/server/agent-chat/framework-prompts.ts +3 -1
- package/corpus/core/src/server/agent-chat/plugin-options.ts +9 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +12 -3
- package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +512 -0
- package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +189 -0
- package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +89 -0
- package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +221 -0
- package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +263 -0
- package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +160 -0
- package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +232 -0
- package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +280 -0
- package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +118 -0
- package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +119 -0
- package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +100 -0
- package/corpus/core/src/templates/chat/.env.example +5 -0
- package/corpus/core/src/templates/chat/.ignore +0 -0
- package/corpus/core/src/templates/chat/.oxfmtrc.json +8 -0
- package/corpus/core/src/templates/chat/AGENTS.md +69 -0
- package/corpus/core/src/templates/chat/CHANGELOG.md +10 -0
- package/corpus/core/src/templates/chat/DEVELOPING.md +185 -0
- package/corpus/core/src/templates/chat/README.md +32 -0
- package/corpus/core/src/templates/chat/_gitignore +43 -0
- package/corpus/core/src/templates/chat/actions/hello.ts +13 -0
- package/corpus/core/src/templates/chat/actions/navigate.ts +41 -0
- package/corpus/core/src/templates/chat/actions/run.ts +2 -0
- package/corpus/core/src/templates/chat/actions/view-screen.ts +31 -0
- package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +60 -0
- package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +185 -0
- package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +450 -0
- package/corpus/core/src/templates/chat/app/components/ui/button.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/card.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/input.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/label.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +1 -0
- package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +1 -0
- package/corpus/core/src/templates/chat/app/design-system.ts +3 -0
- package/corpus/core/src/templates/chat/app/entry.client.tsx +19 -0
- package/corpus/core/src/templates/chat/app/entry.server.tsx +10 -0
- package/corpus/core/src/templates/chat/app/global.css +93 -0
- package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +93 -0
- package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +68 -0
- package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +71 -0
- package/corpus/core/src/templates/chat/app/i18n/en-US.ts +70 -0
- package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +71 -0
- package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +71 -0
- package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +69 -0
- package/corpus/core/src/templates/chat/app/i18n/index.ts +34 -0
- package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +70 -0
- package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +70 -0
- package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +71 -0
- package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +66 -0
- package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +66 -0
- package/corpus/core/src/templates/chat/app/i18n-data.ts +556 -0
- package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +49 -0
- package/corpus/core/src/templates/chat/app/lib/app-config.ts +11 -0
- package/corpus/core/src/templates/chat/app/lib/tab-id.ts +1 -0
- package/corpus/core/src/templates/chat/app/lib/utils.ts +1 -0
- package/corpus/core/src/templates/chat/app/root.tsx +173 -0
- package/corpus/core/src/templates/chat/app/routes/_index.tsx +94 -0
- package/corpus/core/src/templates/chat/app/routes/agent.tsx +24 -0
- package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/core/src/templates/chat/app/routes/database.tsx +17 -0
- package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +2 -0
- package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +11 -0
- package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +11 -0
- package/corpus/core/src/templates/chat/app/routes/extensions.tsx +5 -0
- package/corpus/core/src/templates/chat/app/routes/observability.tsx +19 -0
- package/corpus/core/src/templates/chat/app/routes/settings.tsx +89 -0
- package/corpus/core/src/templates/chat/app/routes/team.tsx +11 -0
- package/corpus/core/src/templates/chat/app/routes.ts +4 -0
- package/corpus/core/src/templates/chat/app/vite-env.d.ts +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
- package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/corpus/core/src/templates/chat/components.json +20 -0
- package/corpus/core/src/templates/chat/learnings.defaults.md +5 -0
- package/corpus/core/src/templates/chat/netlify.toml +10 -0
- package/corpus/core/src/templates/chat/package.json +92 -0
- package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +10 -0
- package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +10 -0
- package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +21 -0
- package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +21 -0
- package/corpus/core/src/templates/chat/public/favicon.svg +1 -0
- package/corpus/core/src/templates/chat/public/icon-180.svg +1 -0
- package/corpus/core/src/templates/chat/public/icon-192.svg +1 -0
- package/corpus/core/src/templates/chat/public/icon-512.svg +1 -0
- package/corpus/core/src/templates/chat/public/manifest.json +21 -0
- package/corpus/core/src/templates/chat/react-router.config.ts +7 -0
- package/corpus/core/src/templates/chat/server/middleware/auth.ts +15 -0
- package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +21 -0
- package/corpus/core/src/templates/chat/server/plugins/auth.ts +17 -0
- package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +5 -0
- package/corpus/core/src/templates/chat/ssr-entry.ts +15 -0
- package/corpus/core/src/templates/chat/tsconfig.json +21 -0
- package/corpus/core/src/templates/chat/vite.config.ts +19 -0
- package/corpus/templates/analytics/AGENTS.md +19 -11
- package/corpus/templates/analytics/actions/list-data-dictionary.ts +3 -1
- package/corpus/templates/analytics/actions/save-data-dictionary-entry.ts +14 -0
- package/corpus/templates/analytics/actions/search-analytics-query-catalog.ts +41 -0
- package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +13 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +12 -11
- package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +7 -1
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +8 -1
- package/corpus/templates/analytics/changelog/2026-07-22-fixed-dashboards-and-analyses-missing-a-copyable-share-link.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-dashboard-chart-loading-placeholders-are-easier-to-see-in-da.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-routine-analytics-lookups-reuse-existing-query-definitions.md +6 -0
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +1 -12
- package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +405 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -16
- package/corpus/templates/content/changelog/2026-07-23-database-toolbar-sort-and-filter-controls-open-reliably.md +6 -0
- package/corpus/templates/forms/app/lib/public-form-link.ts +14 -0
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +6 -14
- package/corpus/templates/forms/changelog/2026-07-23-published-form-links-can-be-copied-without-being-asked-to-re.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-23-scheduled-emails-now-send-only-once-when-multiple-requests-r.md +6 -0
- package/corpus/templates/mail/server/lib/jobs.ts +13 -4
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +1 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +7 -18
- package/corpus/templates/slides/app/lib/agent-visible-message.ts +28 -0
- package/corpus/templates/slides/app/pages/Index.tsx +6 -34
- package/corpus/templates/slides/changelog/2026-07-23-full-prompts-stay-visible-in-chat-when-creating-decks-or-add.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +13 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/SharedRichEditor.tsx +23 -2
- package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +68 -2
- package/corpus/toolkit/src/editor/index.ts +1 -0
- package/corpus/toolkit/src/editor.css +75 -0
- package/corpus/toolkit/src/ui/button.tsx +3 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +96 -115
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +9 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +26 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +2 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +69 -9
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/cli/create.d.ts +2 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +43 -6
- package/dist/cli/create.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +32 -10
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +2 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +9 -3
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +4 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +61 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +10 -9
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
- package/dist/server/agent-chat/framework-prompts.js +3 -1
- package/dist/server/agent-chat/framework-prompts.js.map +1 -1
- package/dist/server/agent-chat/plugin-options.d.ts +9 -0
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +11 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +4 -4
- package/src/agent/production-agent.ts +107 -125
- package/src/agent/run-manager.ts +45 -2
- package/src/agent/thread-data-builder.ts +85 -8
- package/src/cli/create.ts +44 -5
- package/src/client/AssistantChat.tsx +37 -9
- package/src/client/chat/message-components.tsx +9 -1
- package/src/client/chat/repo-helpers.ts +77 -0
- package/src/client/chat/tool-call-display.tsx +51 -15
- package/src/server/agent-chat/framework-prompts.ts +3 -1
- package/src/server/agent-chat/plugin-options.ts +9 -0
- package/src/server/agent-chat-plugin.ts +12 -3
- package/src/templates/chat/actions/hello.spec.ts +0 -23
- package/src/templates/chat/actions/navigate.spec.ts +0 -68
- package/src/templates/chat/actions/view-screen.spec.ts +0 -40
- package/src/templates/chat/app/lib/agent-page.spec.tsx +0 -61
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { getAllSettings, listOrgSettings } from "@agent-native/core/settings";
|
|
2
|
+
|
|
3
|
+
import { dashboardCatalogEntries } from "./dashboard-catalog";
|
|
4
|
+
import { listDashboards, type DashboardRecord } from "./dashboards-store";
|
|
5
|
+
|
|
6
|
+
const DATA_DICTIONARY_KEY_PREFIX = "data-dict-";
|
|
7
|
+
const MAX_QUERY_LENGTH = 12_000;
|
|
8
|
+
const STOP_WORDS = new Set([
|
|
9
|
+
"a",
|
|
10
|
+
"all",
|
|
11
|
+
"an",
|
|
12
|
+
"and",
|
|
13
|
+
"are",
|
|
14
|
+
"by",
|
|
15
|
+
"count",
|
|
16
|
+
"data",
|
|
17
|
+
"day",
|
|
18
|
+
"days",
|
|
19
|
+
"do",
|
|
20
|
+
"exact",
|
|
21
|
+
"find",
|
|
22
|
+
"for",
|
|
23
|
+
"from",
|
|
24
|
+
"get",
|
|
25
|
+
"give",
|
|
26
|
+
"how",
|
|
27
|
+
"i",
|
|
28
|
+
"in",
|
|
29
|
+
"last",
|
|
30
|
+
"look",
|
|
31
|
+
"many",
|
|
32
|
+
"me",
|
|
33
|
+
"metric",
|
|
34
|
+
"number",
|
|
35
|
+
"of",
|
|
36
|
+
"on",
|
|
37
|
+
"over",
|
|
38
|
+
"our",
|
|
39
|
+
"please",
|
|
40
|
+
"show",
|
|
41
|
+
"that",
|
|
42
|
+
"the",
|
|
43
|
+
"this",
|
|
44
|
+
"time",
|
|
45
|
+
"today",
|
|
46
|
+
"total",
|
|
47
|
+
"up",
|
|
48
|
+
"week",
|
|
49
|
+
"what",
|
|
50
|
+
"were",
|
|
51
|
+
"we",
|
|
52
|
+
"yesterday",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
type DictionaryEntry = Record<string, unknown>;
|
|
56
|
+
type DashboardPanel = Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
export type AnalyticsQueryCatalogCandidate =
|
|
59
|
+
| {
|
|
60
|
+
kind: "dashboard-panel";
|
|
61
|
+
origin: "saved-dashboard" | "dashboard-template";
|
|
62
|
+
score: number;
|
|
63
|
+
matchedTerms: string[];
|
|
64
|
+
dashboardId: string;
|
|
65
|
+
dashboardTitle: string;
|
|
66
|
+
dashboardDescription?: string;
|
|
67
|
+
panelId: string;
|
|
68
|
+
panelTitle: string;
|
|
69
|
+
panelDescription?: string;
|
|
70
|
+
source?: string;
|
|
71
|
+
query: string | Record<string, unknown>;
|
|
72
|
+
timeScope?: string;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
kind: "data-dictionary";
|
|
76
|
+
origin: "data-dictionary";
|
|
77
|
+
score: number;
|
|
78
|
+
matchedTerms: string[];
|
|
79
|
+
id: string;
|
|
80
|
+
metric: string;
|
|
81
|
+
definition?: string;
|
|
82
|
+
source?: string;
|
|
83
|
+
action?: string;
|
|
84
|
+
table?: string;
|
|
85
|
+
columnsUsed?: string;
|
|
86
|
+
queryTemplate?: string;
|
|
87
|
+
knownGotchas?: string;
|
|
88
|
+
approved?: boolean;
|
|
89
|
+
aiGenerated?: boolean;
|
|
90
|
+
sourceUrl?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function text(value: unknown): string {
|
|
94
|
+
return typeof value === "string" ? value.trim() : "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compactQuery(value: unknown): string | Record<string, unknown> | null {
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
return trimmed ? trimmed.slice(0, MAX_QUERY_LENGTH) : null;
|
|
101
|
+
}
|
|
102
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
103
|
+
return value as Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function searchTerms(search: string): string[] {
|
|
109
|
+
const normalized = search
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
112
|
+
.trim();
|
|
113
|
+
if (!normalized) return [];
|
|
114
|
+
const meaningful = normalized
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.filter((term) => term.length > 1 && !STOP_WORDS.has(term));
|
|
117
|
+
return meaningful.length ? Array.from(new Set(meaningful)) : [normalized];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function matchScore(
|
|
121
|
+
search: string,
|
|
122
|
+
weightedFields: Array<{ value: unknown; weight: number }>,
|
|
123
|
+
): { score: number; matchedTerms: string[] } {
|
|
124
|
+
const terms = searchTerms(search);
|
|
125
|
+
if (!terms.length) return { score: 0, matchedTerms: [] };
|
|
126
|
+
|
|
127
|
+
const normalizedSearch = search.toLowerCase().trim();
|
|
128
|
+
const matched = new Set<string>();
|
|
129
|
+
let score = 0;
|
|
130
|
+
for (const field of weightedFields) {
|
|
131
|
+
const value = text(field.value).toLowerCase();
|
|
132
|
+
if (!value) continue;
|
|
133
|
+
if (normalizedSearch.length > 2 && value.includes(normalizedSearch)) {
|
|
134
|
+
score += field.weight * 4;
|
|
135
|
+
}
|
|
136
|
+
for (const term of terms) {
|
|
137
|
+
if (!value.includes(term)) continue;
|
|
138
|
+
matched.add(term);
|
|
139
|
+
score += field.weight;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (matched.size === terms.length) score += 40;
|
|
144
|
+
return { score, matchedTerms: [...matched] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dashboardPanelCandidates(args: {
|
|
148
|
+
dashboardId: string;
|
|
149
|
+
dashboardTitle: string;
|
|
150
|
+
dashboardDescription?: string;
|
|
151
|
+
config: Record<string, unknown>;
|
|
152
|
+
origin: "saved-dashboard" | "dashboard-template";
|
|
153
|
+
search: string;
|
|
154
|
+
}): AnalyticsQueryCatalogCandidate[] {
|
|
155
|
+
const panels = Array.isArray(args.config.panels)
|
|
156
|
+
? (args.config.panels as DashboardPanel[])
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
return panels.flatMap((panel) => {
|
|
160
|
+
const query = compactQuery(panel.sql);
|
|
161
|
+
if (!query) return [];
|
|
162
|
+
const panelConfig =
|
|
163
|
+
panel.config &&
|
|
164
|
+
typeof panel.config === "object" &&
|
|
165
|
+
!Array.isArray(panel.config)
|
|
166
|
+
? (panel.config as Record<string, unknown>)
|
|
167
|
+
: {};
|
|
168
|
+
const panelTitle = text(panel.title) || text(panel.id);
|
|
169
|
+
const panelDescription = text(panelConfig.description);
|
|
170
|
+
const { score: rawScore, matchedTerms } = matchScore(args.search, [
|
|
171
|
+
{ value: panelTitle, weight: 24 },
|
|
172
|
+
{ value: panelDescription, weight: 12 },
|
|
173
|
+
{ value: args.dashboardTitle, weight: 10 },
|
|
174
|
+
{ value: args.dashboardDescription, weight: 6 },
|
|
175
|
+
{ value: panel.source, weight: 5 },
|
|
176
|
+
{
|
|
177
|
+
value: typeof query === "string" ? query : JSON.stringify(query),
|
|
178
|
+
weight: 2,
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
const requestedTerms = new Set(searchTerms(args.search));
|
|
182
|
+
const titleSpecificityPenalty =
|
|
183
|
+
searchTerms(panelTitle).filter((term) => !requestedTerms.has(term))
|
|
184
|
+
.length * 12;
|
|
185
|
+
const aggregateIntent =
|
|
186
|
+
/\b(how many|count|number|total)\b/i.test(args.search) &&
|
|
187
|
+
text(panel.chartType) === "metric"
|
|
188
|
+
? 20
|
|
189
|
+
: 0;
|
|
190
|
+
const score = rawScore - titleSpecificityPenalty + aggregateIntent;
|
|
191
|
+
if (score <= 0) return [];
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
kind: "dashboard-panel" as const,
|
|
196
|
+
origin: args.origin,
|
|
197
|
+
score,
|
|
198
|
+
matchedTerms,
|
|
199
|
+
dashboardId: args.dashboardId,
|
|
200
|
+
dashboardTitle: args.dashboardTitle,
|
|
201
|
+
...(args.dashboardDescription
|
|
202
|
+
? { dashboardDescription: args.dashboardDescription }
|
|
203
|
+
: {}),
|
|
204
|
+
panelId: text(panel.id),
|
|
205
|
+
panelTitle,
|
|
206
|
+
...(panelDescription ? { panelDescription } : {}),
|
|
207
|
+
...(text(panel.source) ? { source: text(panel.source) } : {}),
|
|
208
|
+
query,
|
|
209
|
+
...(text(panelConfig.timeScope)
|
|
210
|
+
? { timeScope: text(panelConfig.timeScope) }
|
|
211
|
+
: {}),
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function dictionaryCandidates(
|
|
218
|
+
entries: DictionaryEntry[],
|
|
219
|
+
search: string,
|
|
220
|
+
): AnalyticsQueryCatalogCandidate[] {
|
|
221
|
+
return entries.flatMap((entry) => {
|
|
222
|
+
const { score, matchedTerms } = matchScore(search, [
|
|
223
|
+
{ value: entry.metric, weight: 28 },
|
|
224
|
+
{ value: entry.commonQuestions, weight: 16 },
|
|
225
|
+
{ value: entry.definition, weight: 12 },
|
|
226
|
+
{ value: entry.table, weight: 8 },
|
|
227
|
+
{ value: entry.columnsUsed, weight: 6 },
|
|
228
|
+
{ value: entry.queryTemplate, weight: 5 },
|
|
229
|
+
{ value: entry.source, weight: 5 },
|
|
230
|
+
{ value: entry.action, weight: 5 },
|
|
231
|
+
{ value: entry.knownGotchas, weight: 2 },
|
|
232
|
+
]);
|
|
233
|
+
if (!score) return [];
|
|
234
|
+
|
|
235
|
+
const id = text(entry.id);
|
|
236
|
+
const metric = text(entry.metric);
|
|
237
|
+
if (!id || !metric) return [];
|
|
238
|
+
return [
|
|
239
|
+
{
|
|
240
|
+
kind: "data-dictionary" as const,
|
|
241
|
+
origin: "data-dictionary" as const,
|
|
242
|
+
score: score + (entry.approved === true ? 12 : 0),
|
|
243
|
+
matchedTerms,
|
|
244
|
+
id,
|
|
245
|
+
metric,
|
|
246
|
+
...(text(entry.definition)
|
|
247
|
+
? { definition: text(entry.definition) }
|
|
248
|
+
: {}),
|
|
249
|
+
...(text(entry.source) ? { source: text(entry.source) } : {}),
|
|
250
|
+
...(text(entry.action) ? { action: text(entry.action) } : {}),
|
|
251
|
+
...(text(entry.table) ? { table: text(entry.table) } : {}),
|
|
252
|
+
...(text(entry.columnsUsed)
|
|
253
|
+
? { columnsUsed: text(entry.columnsUsed) }
|
|
254
|
+
: {}),
|
|
255
|
+
...(text(entry.queryTemplate)
|
|
256
|
+
? { queryTemplate: text(entry.queryTemplate) }
|
|
257
|
+
: {}),
|
|
258
|
+
...(text(entry.knownGotchas)
|
|
259
|
+
? { knownGotchas: text(entry.knownGotchas) }
|
|
260
|
+
: {}),
|
|
261
|
+
...(typeof entry.approved === "boolean"
|
|
262
|
+
? { approved: entry.approved }
|
|
263
|
+
: {}),
|
|
264
|
+
...(typeof entry.aiGenerated === "boolean"
|
|
265
|
+
? { aiGenerated: entry.aiGenerated }
|
|
266
|
+
: {}),
|
|
267
|
+
...(text(entry.sourceUrl) ? { sourceUrl: text(entry.sourceUrl) } : {}),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function rankAnalyticsQueryCatalog(args: {
|
|
274
|
+
search: string;
|
|
275
|
+
dashboards: Array<{
|
|
276
|
+
id: string;
|
|
277
|
+
title: string;
|
|
278
|
+
description?: string;
|
|
279
|
+
config: Record<string, unknown>;
|
|
280
|
+
origin: "saved-dashboard" | "dashboard-template";
|
|
281
|
+
}>;
|
|
282
|
+
dictionaryEntries: DictionaryEntry[];
|
|
283
|
+
limit: number;
|
|
284
|
+
}): AnalyticsQueryCatalogCandidate[] {
|
|
285
|
+
const candidates = [
|
|
286
|
+
...args.dashboards.flatMap((dashboard) =>
|
|
287
|
+
dashboardPanelCandidates({
|
|
288
|
+
dashboardId: dashboard.id,
|
|
289
|
+
dashboardTitle: dashboard.title,
|
|
290
|
+
dashboardDescription: dashboard.description,
|
|
291
|
+
config: dashboard.config,
|
|
292
|
+
origin: dashboard.origin,
|
|
293
|
+
search: args.search,
|
|
294
|
+
}),
|
|
295
|
+
),
|
|
296
|
+
...dictionaryCandidates(args.dictionaryEntries, args.search),
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
return candidates
|
|
300
|
+
.sort((a, b) => {
|
|
301
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
302
|
+
if (a.kind !== b.kind) return a.kind === "data-dictionary" ? -1 : 1;
|
|
303
|
+
return JSON.stringify(a).localeCompare(JSON.stringify(b));
|
|
304
|
+
})
|
|
305
|
+
.slice(0, args.limit);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function listDictionaryEntries(args: {
|
|
309
|
+
email: string;
|
|
310
|
+
orgId: string | null;
|
|
311
|
+
}): Promise<DictionaryEntry[]> {
|
|
312
|
+
const entries: DictionaryEntry[] = [];
|
|
313
|
+
const seen = new Set<string>();
|
|
314
|
+
const collect = (raw: unknown) => {
|
|
315
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
316
|
+
const entry = raw as DictionaryEntry;
|
|
317
|
+
const id = text(entry.id);
|
|
318
|
+
if (!id || seen.has(id)) return;
|
|
319
|
+
seen.add(id);
|
|
320
|
+
entries.push(entry);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (args.orgId) {
|
|
324
|
+
const orgEntries = await listOrgSettings(
|
|
325
|
+
args.orgId,
|
|
326
|
+
DATA_DICTIONARY_KEY_PREFIX,
|
|
327
|
+
);
|
|
328
|
+
for (const value of Object.values(orgEntries)) collect(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const userPrefix = `u:${args.email}:${DATA_DICTIONARY_KEY_PREFIX}`;
|
|
332
|
+
const all = await getAllSettings();
|
|
333
|
+
for (const [key, value] of Object.entries(all)) {
|
|
334
|
+
if (key.startsWith(userPrefix)) collect(value);
|
|
335
|
+
}
|
|
336
|
+
return entries;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function savedDashboardInput(dashboard: DashboardRecord) {
|
|
340
|
+
const config = dashboard.config as Record<string, unknown>;
|
|
341
|
+
return {
|
|
342
|
+
id: dashboard.id,
|
|
343
|
+
title: text(config.name) || dashboard.title,
|
|
344
|
+
description: text(config.description) || undefined,
|
|
345
|
+
config,
|
|
346
|
+
origin: "saved-dashboard" as const,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function searchAnalyticsQueryCatalog(args: {
|
|
351
|
+
search: string;
|
|
352
|
+
email: string;
|
|
353
|
+
orgId: string | null;
|
|
354
|
+
limit: number;
|
|
355
|
+
}): Promise<AnalyticsQueryCatalogCandidate[]> {
|
|
356
|
+
const [savedDashboardsResult, dictionaryEntriesResult] =
|
|
357
|
+
await Promise.allSettled([
|
|
358
|
+
listDashboards(
|
|
359
|
+
{ email: args.email, orgId: args.orgId },
|
|
360
|
+
{ kind: "sql", archived: "active", hidden: "visible" },
|
|
361
|
+
),
|
|
362
|
+
listDictionaryEntries({ email: args.email, orgId: args.orgId }),
|
|
363
|
+
]);
|
|
364
|
+
const savedDashboards =
|
|
365
|
+
savedDashboardsResult.status === "fulfilled"
|
|
366
|
+
? savedDashboardsResult.value
|
|
367
|
+
: [];
|
|
368
|
+
const dictionaryEntries =
|
|
369
|
+
dictionaryEntriesResult.status === "fulfilled"
|
|
370
|
+
? dictionaryEntriesResult.value
|
|
371
|
+
: [];
|
|
372
|
+
|
|
373
|
+
const savedIds = new Set(savedDashboards.map((dashboard) => dashboard.id));
|
|
374
|
+
const templateDashboards = dashboardCatalogEntries
|
|
375
|
+
.filter((entry) => !savedIds.has(entry.defaultDashboardId))
|
|
376
|
+
.flatMap((entry) => {
|
|
377
|
+
try {
|
|
378
|
+
const config = entry.buildConfig() as unknown as Record<
|
|
379
|
+
string,
|
|
380
|
+
unknown
|
|
381
|
+
>;
|
|
382
|
+
return [
|
|
383
|
+
{
|
|
384
|
+
id: entry.defaultDashboardId,
|
|
385
|
+
title: text(config.name) || entry.name,
|
|
386
|
+
description: text(config.description) || entry.description,
|
|
387
|
+
config,
|
|
388
|
+
origin: "dashboard-template" as const,
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
} catch {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return rankAnalyticsQueryCatalog({
|
|
397
|
+
search: args.search,
|
|
398
|
+
dashboards: [
|
|
399
|
+
...savedDashboards.map(savedDashboardInput),
|
|
400
|
+
...templateDashboards,
|
|
401
|
+
],
|
|
402
|
+
dictionaryEntries,
|
|
403
|
+
limit: args.limit,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
@@ -46,11 +46,11 @@ const ANALYTICS_DATA_SOURCES_LINK = buildDeepLink({
|
|
|
46
46
|
to: "/data-sources",
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
export const
|
|
50
|
-
"
|
|
49
|
+
export const BOUNDED_STRUCTURED_LOOKUP_GUIDANCE =
|
|
50
|
+
"BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect only the most likely source schema or ask one clarification; do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
|
|
51
51
|
|
|
52
52
|
export const BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE =
|
|
53
|
-
"BUILT-IN FIRST-PARTY SOURCE — Analytics always provides one built-in first-party source alongside connected external providers such as BigQuery, HubSpot, Gong, Slack, and the other configured integrations. This does not replace or restrict external sources.
|
|
53
|
+
"BUILT-IN FIRST-PARTY SOURCE — Analytics always provides one built-in first-party source alongside connected external providers such as BigQuery, HubSpot, Gong, Slack, and the other configured integrations. This does not replace or restrict external sources. When `search-analytics-query-catalog` identifies a first-party dashboard/chart definition, preserve its event semantics and use `query-agent-native-analytics` over `analytics_events` or `session_recordings` as appropriate. When the user names an external provider, or the catalog identifies one as authoritative, query that provider instead. Do not report the first-party source as disconnected merely because an external provider is not configured. If the authoritative query returns no rows, report that grounded result with its scope and time window. ";
|
|
54
54
|
|
|
55
55
|
export const ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE =
|
|
56
56
|
"OBSERVABILITY INCIDENT WORKFLOW — For a named user's session or error question, resolve the user's email from context, then use list-session-recordings with userId over a bounded recent window to discover the relevant sessions. Do not require hasErrors=true for this initial lookup: replay/network/stuck-run evidence can exist while the recording's JavaScript errorCount is zero. Use hasErrors=true only when the user specifically asks for recordings with captured JavaScript errors or the recording metadata confirms that filter is appropriate. Use list-error-issues with userId or sessionRecordingId to identify a grouped issue, then get-error-issue for stack, breadcrumbs, occurrences, and linked recordings. For console diagnostics or failed network requests, create-session-replay-agent-link first and use its scoped diagnostics endpoint for detailed error text, stacks, request metadata, and bounded 5xx snippets; enumerate with kind/limit and fromMs/toMs or offset when needed. Use get-session-replay-summary and get-session-replay-timeline for the page-navigation and click sequence, and use get-session-replay-events only for additional bounded replay-event details. If no grouped error exists, correlate first-party observability events such as agent_chat_stuck_detected with query-agent-native-analytics in execution mode. In Plan mode, query-agent-native-analytics is intentionally unavailable: do not claim a live event correlation or root cause from it, and describe the event lookup as a planned next step. Prefer these first-party actions over generic SQL. Report the matching evidence and do not claim a root cause without a corroborating error, event, or replay signal. ";
|
|
@@ -75,7 +75,7 @@ export function analyticsSourceGuidanceOpening(): string {
|
|
|
75
75
|
"<data-source-guidance>\n" +
|
|
76
76
|
"Apply real-data requirements only when presenting analytics results, source records, or derived metrics. Do not call data-source tools for workflow migration, recurring-job setup, UI/code fixes, settings help, conceptual planning, or other non-data tasks unless the user explicitly asks for data. " +
|
|
77
77
|
NON_ANALYTICS_REQUEST_GUIDANCE +
|
|
78
|
-
|
|
78
|
+
BOUNDED_STRUCTURED_LOOKUP_GUIDANCE +
|
|
79
79
|
BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
|
|
80
80
|
ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
|
|
81
81
|
`DATA-SOURCE SETUP UX — Chat remains available when no external data source is connected. For a live-data request that needs an unavailable external provider, explain what is missing in the context of the user's question and guide them naturally to [Connect data sources](${ANALYTICS_DATA_SOURCES_LINK}). Use that real link from the app; do not emit a generic canned no-data sentence. For general conversation, conceptual questions, and questions the built-in first-party source can answer, continue helping normally. ` +
|
|
@@ -85,7 +85,7 @@ export function analyticsSourceGuidanceOpening(): string {
|
|
|
85
85
|
|
|
86
86
|
export function analyticsDataDictionaryRoutingContext(): string {
|
|
87
87
|
return `<data-dictionary-routing>
|
|
88
|
-
Data-dictionary definitions are available
|
|
88
|
+
Data-dictionary definitions are available through \`search-analytics-query-catalog\`, which combines focused dictionary lookup with a search over existing dashboard/chart SQL. Use that combined catalog search as the normal preflight for a bounded metric lookup. Call \`list-data-dictionary\` separately only when the user asks to browse definitions or filter them by department. Treat approved entries as canonical, verify unreviewed human entries when stakes are high, and treat AI-generated unapproved entries as suggestions only. After the catalog identifies one source and query shape, query that source once and stop on success. If no matching definition or chart exists, inspect only the most likely source schema or ask the user instead of fanning out across providers.
|
|
89
89
|
</data-dictionary-routing>`;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -675,6 +675,7 @@ export default createAgentChatPlugin({
|
|
|
675
675
|
loadActionsFromStaticRegistry(actionsRegistry),
|
|
676
676
|
),
|
|
677
677
|
initialToolNames: INITIAL_TOOL_NAMES,
|
|
678
|
+
corpusTools: "lazy",
|
|
678
679
|
finalResponseGuard: realDataFinalGuard,
|
|
679
680
|
// Enable sandboxed JavaScript execution for analytics data processing.
|
|
680
681
|
// Code runs in an isolated Node.js child process with no access to app
|
|
@@ -717,18 +718,12 @@ export default createAgentChatPlugin({
|
|
|
717
718
|
'DASHBOARD REORDER RULE — For simple chart/section moves, use `mutate-dashboard` code such as `dashboard.panels(["panel-a","panel-b"]).moveToTop();`. For visible placement requests like "second row" or "next to return rates", use row-aware placement such as `dashboard.insertPanel({...}).nextTo("retention-over-time")`, `.atRow(2)`, or `dashboard.panel("panel-a").moveNextTo("panel-b")`; these keep panels in the intended rendered row and expand/rebalance that row when needed. Never count shifting `/panels/<index>` positions for ordinary \'move this chart\' requests. Use `get-sql-dashboard.layout.groups[].rows` as proof of visible row placement, not only flat `panelOrder`. ' +
|
|
718
719
|
"Use configured data sources and actions only. The built-in first-party Analytics source is an additional source and is always available through `query-agent-native-analytics`, even when no external provider credentials are connected. External provider actions remain available and are the authoritative path when the user names a provider or the data lives there. Call `data-source-status` when you need to know which external providers are connected, and treat provider actions as unavailable for analysis only if they return missing credentials, permission, syntax, quota, or network errors. " +
|
|
719
720
|
"The built-in `demo` dashboard source is a demo-environment Prometheus source reserved for the Node Exporter demo. It must never satisfy REAL_DATA_REQUIRED or be cited as user analytics evidence unless the user explicitly asks to inspect the demo dashboard. " +
|
|
720
|
-
"When the user names a provider
|
|
721
|
-
"
|
|
722
|
-
"
|
|
723
|
-
'For broad unstructured provider records such as transcripts, messages, tickets, issues, notes, events, documents, or conversation logs, prefer `provider-corpus-job` so the scan has durable checkpoints, stored snippets, coverage counts, and provider quota pause/resume. Use mode="paginated-search" for any provider endpoint that already pages over the target records; use mode="batch-search" when a staged upstream cohort of ids/records must feed a second provider endpoint by `itemBodyPath` or `itemQueryParam`. Continue paused jobs until completed or quota_wait, then read results. Use `run-code` with `providerSearchAll`, `providerFetch`, `appAction`, `workspaceRead`, and Resources-backed workspace files for shorter reductions, joins, classification, and aggregation after the corpus path is established. Convenience actions are bounded shortcuts for common checks, not the ceiling for what the underlying provider API can answer. ' +
|
|
724
|
-
"When the user asks to search source-record body text, use the raw body endpoint or native provider search for that record type. Parent/container metadata such as call lists, titles, summaries, briefs, channel lists, ticket lists, or issue lists can discover scope but cannot support a complete body-text absence claim. " +
|
|
725
|
-
"For source-record mention searches, phrase searches, and absence checks across transcripts/messages/tickets/issues, prefer provider-side search, `provider-corpus-job`, staged corpus, `providerSearchAll`, or action-side search arguments that return counts, coverage, and snippets instead of loading raw full records into chat. Use full transcripts/messages only for selected evidence after the search narrows the corpus, and state whether the search covered every matching record or only a bounded sample. " +
|
|
726
|
-
"For complex provider questions, broad searches, corpus-wide counts, cross-source joins, or any answer where absence matters, prefer a corpus-first workflow: inspect the provider API, fetch every relevant page or an explicitly bounded cohort, stage large responses with `saveToFile`/`stageAs`/`fetchAllPages`, use `provider-corpus-job` for durable long-running searches, and use `run-code` with `providerSearchAll`, `providerFetch`, `appAction`, and Resources-backed workspace files to join, search, classify, and aggregate. Use `scratch/` for temporary staging and durable folder names only for files the user should keep. Do not infer no results from sampled records, default limits, truncated excerpts, or aborted calls. If full coverage is not possible in the turn, say exactly what was inspected and what remains uncovered. " +
|
|
727
|
-
'For HubSpot deal cohorts, structured `hubspot-deals` filters can enumerate a bounded cohort for direct deal-list answers: `product` for the `products` field, `pipeline` for pipeline label/id, `closedStatus` for won/lost/open/closed, and `closedDateFrom`/`closedDateTo` for close-date windows. The `query` argument is full-text deal search and is not proof that a specific property matched; do not use `query: "Publish"` when the user asked for products field = Publish. If the cohort feeds transcript/message/ticket search, a cross-source join, or exhaustive/absence-sensitive coverage, use `provider-api-request` with HubSpot search and `stageAs` for the cohort first, then continue with the provider API/corpus workflow. Report the returned filter values and cohort count in the methodology. ' +
|
|
728
|
-
"For named deal, account, renewal, churn-risk, or customer deep dives that need HubSpot and Gong context, `account-deep-dive` can provide a bounded evidence bundle. Use it when the user asks for a targeted entity deep dive, then use the broad provider API/corpus workflow for any requested exhaustive transcript/message/ticket search or absence claim. Do not answer a requested transcript deep dive from call metadata alone. " +
|
|
721
|
+
"When the user names a provider such as first-party Analytics, BigQuery, HubSpot, Gong, Jira, Pylon, Slack, Sentry, GA4, or another connected source, that source is authoritative for the turn. Use its first-class query action when available; if it is not on the initial tool surface, use tool-search for that provider instead of loading unrelated catalogs. For an ordinary structured lookup, make one bounded query and stop on success. " +
|
|
722
|
+
"Load provider API, corpus, staging, or code tools only when the user explicitly requests cross-source work, exhaustive unstructured-record coverage, or an absence claim that the first-class action cannot support. For those genuinely broad workflows, fetch every relevant page or an explicitly bounded cohort, preserve coverage counts, and state any uncovered records. " +
|
|
723
|
+
"For named deal, account, renewal, churn-risk, or customer deep dives that need HubSpot and Gong context, `account-deep-dive` can provide a bounded evidence bundle. Do not answer a requested transcript deep dive from call metadata alone. " +
|
|
729
724
|
"When the user refers to the current analysis, this analysis, this project, or asks to spin off, adapt, modify, or reuse a saved analysis, call `view-screen` first and use the returned analysis details; if an analysis id or @mention is provided, call `get-analysis` before responding. " +
|
|
730
|
-
"If a
|
|
731
|
-
"For ordinary ad-hoc
|
|
725
|
+
"If a query action fails because its arguments are invalid, correct the arguments once. Never repeat the identical failed call. For credential, permission, quota, network, or repeated schema failures, stop using that source for the turn and surface the actual error instead of trying unrelated providers. " +
|
|
726
|
+
"For ordinary ad-hoc structured data questions, answer the explicit question after the first relevant successful query or bounded evidence batch. The words all, total, or exact do not require cross-source validation when a single structured query fully covers the requested source and filters. " +
|
|
732
727
|
"If the user challenges coverage, asks why more records were not included, or asks for the updated answer, rerun the relevant source query or revise from the corrected cohort and provide the updated deliverable directly. Do not claim an analysis was revised unless the revised answer is included in the response or saved with `save-analysis`. " +
|
|
733
728
|
"Unstructured source records are valid analytics evidence: Pylon tickets, Jira issues, Gong calls/transcripts, Slack messages, and similar text records may be coded for themes, mention counts, sentiment, objections, and qualitative patterns as long as the answer states the inspected sample size and does not imply unsupported statistical certainty. " +
|
|
734
729
|
"For schema questions, prefer data-dictionary entries and configured warehouse schemas over assumptions; use `search-bigquery-schema` for BigQuery metadata before inventing datasets, tables, or columns. " +
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { appPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
|
|
3
|
+
interface PublicFormLink {
|
|
4
|
+
status: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getPublishedFormUrl(
|
|
9
|
+
form: PublicFormLink,
|
|
10
|
+
origin: string,
|
|
11
|
+
): string | undefined {
|
|
12
|
+
if (form.status !== "published") return undefined;
|
|
13
|
+
return `${origin}${appPath(`/f/${form.slug}`)}`;
|
|
14
|
+
}
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
type FormBuilderTab,
|
|
86
86
|
} from "@/lib/form-builder-tabs";
|
|
87
87
|
import { normalizeFields } from "@/lib/normalize-fields";
|
|
88
|
+
import { getPublishedFormUrl } from "@/lib/public-form-link";
|
|
88
89
|
import { cn } from "@/lib/utils";
|
|
89
90
|
|
|
90
91
|
type Translator = ReturnType<typeof useT>;
|
|
@@ -175,8 +176,8 @@ export function FormBuilderPage() {
|
|
|
175
176
|
const { isLocal } = useDbStatus();
|
|
176
177
|
const [showCloudUpgrade, setShowCloudUpgrade] = useState(false);
|
|
177
178
|
const publishedFormUrl =
|
|
178
|
-
form
|
|
179
|
-
?
|
|
179
|
+
form && typeof window !== "undefined"
|
|
180
|
+
? getPublishedFormUrl(form, window.location.origin)
|
|
180
181
|
: undefined;
|
|
181
182
|
const [agentPopoverOpen, setAgentPopoverOpen] = useState(false);
|
|
182
183
|
const [agentPrompt, setAgentPrompt] = useState("");
|
|
@@ -533,16 +534,11 @@ export function FormBuilderPage() {
|
|
|
533
534
|
}
|
|
534
535
|
|
|
535
536
|
function copyShareLink() {
|
|
536
|
-
if (
|
|
537
|
+
if (!publishedFormUrl) {
|
|
537
538
|
toast.info(t("builder.publishBeforeCopyToast"));
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
540
|
-
|
|
541
|
-
setShowCloudUpgrade(true);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const url = `${window.location.origin}${appPath(`/f/${loadedForm.slug}`)}`;
|
|
545
|
-
navigator.clipboard.writeText(url);
|
|
541
|
+
navigator.clipboard.writeText(publishedFormUrl);
|
|
546
542
|
setCopied(true);
|
|
547
543
|
setTimeout(() => setCopied(false), 2000);
|
|
548
544
|
toast.success(t("builder.linkCopiedToast"));
|
|
@@ -611,11 +607,7 @@ export function FormBuilderPage() {
|
|
|
611
607
|
className="h-10 w-10 active:scale-[0.96] motion-reduce:active:scale-100"
|
|
612
608
|
asChild
|
|
613
609
|
>
|
|
614
|
-
<a
|
|
615
|
-
href={appPath(`/f/${form.slug}`)}
|
|
616
|
-
target="_blank"
|
|
617
|
-
rel="noopener"
|
|
618
|
-
>
|
|
610
|
+
<a href={publishedFormUrl} target="_blank" rel="noopener">
|
|
619
611
|
<IconExternalLink className="h-4 w-4" />
|
|
620
612
|
</a>
|
|
621
613
|
</Button>
|
|
@@ -722,7 +722,7 @@ export async function sendScheduledJobNowForOwner(
|
|
|
722
722
|
throw new Error(`Scheduled email is already ${job.status}`);
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
await db
|
|
725
|
+
const claim = await db
|
|
726
726
|
.update(schema.scheduledJobs)
|
|
727
727
|
.set({ status: "processing" } as any)
|
|
728
728
|
.where(
|
|
@@ -732,6 +732,9 @@ export async function sendScheduledJobNowForOwner(
|
|
|
732
732
|
eq(schema.scheduledJobs.status, "pending"),
|
|
733
733
|
),
|
|
734
734
|
);
|
|
735
|
+
if (claim.rowsAffected === 0) {
|
|
736
|
+
throw new Error("Scheduled email is already processing");
|
|
737
|
+
}
|
|
735
738
|
|
|
736
739
|
try {
|
|
737
740
|
await sendScheduledEmail(
|
|
@@ -769,11 +772,17 @@ export async function markJobDone(id: string): Promise<void> {
|
|
|
769
772
|
.where(eq(schema.scheduledJobs.id, id));
|
|
770
773
|
}
|
|
771
774
|
|
|
772
|
-
export async function markJobProcessing(id: string): Promise<
|
|
773
|
-
await db
|
|
775
|
+
export async function markJobProcessing(id: string): Promise<boolean> {
|
|
776
|
+
const result = await db
|
|
774
777
|
.update(schema.scheduledJobs)
|
|
775
778
|
.set({ status: "processing" } as any)
|
|
776
|
-
.where(
|
|
779
|
+
.where(
|
|
780
|
+
and(
|
|
781
|
+
eq(schema.scheduledJobs.id, id),
|
|
782
|
+
eq(schema.scheduledJobs.status, "pending"),
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
return result.rowsAffected > 0;
|
|
777
786
|
}
|
|
778
787
|
|
|
779
788
|
export async function getDuePendingJobs(
|
|
@@ -48,7 +48,7 @@ async function processJobs(): Promise<void> {
|
|
|
48
48
|
const due = await getDuePendingJobs(now);
|
|
49
49
|
|
|
50
50
|
for (const job of due) {
|
|
51
|
-
await markJobProcessing(job.id);
|
|
51
|
+
if (!(await markJobProcessing(job.id))) continue;
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
const ownerEmail = job.ownerEmail || job.accountEmail;
|