@farming-labs/theme 0.1.70 → 0.1.72
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/docs-api.d.mts +4 -1
- package/dist/docs-api.mjs +378 -36
- package/package.json +2 -2
package/dist/docs-api.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
|
|
1
|
+
import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsObservabilityConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
|
|
2
2
|
|
|
3
3
|
//#region src/docs-api.d.ts
|
|
4
4
|
interface AIProviderConfig {
|
|
@@ -43,6 +43,8 @@ interface DocsAPIOptions {
|
|
|
43
43
|
search?: boolean | DocsSearchConfig;
|
|
44
44
|
/** Analytics configuration */
|
|
45
45
|
analytics?: boolean | DocsAnalyticsConfig;
|
|
46
|
+
/** Observability configuration for logs, traces, and metrics callbacks. */
|
|
47
|
+
observability?: boolean | DocsObservabilityConfig;
|
|
46
48
|
/** Feedback configuration */
|
|
47
49
|
feedback?: boolean | FeedbackConfig;
|
|
48
50
|
/** MCP configuration used for the agent discovery spec. */
|
|
@@ -59,6 +61,7 @@ interface DocsMCPAPIOptions {
|
|
|
59
61
|
mcp?: boolean | DocsMcpConfig;
|
|
60
62
|
search?: boolean | DocsSearchConfig;
|
|
61
63
|
analytics?: boolean | DocsAnalyticsConfig;
|
|
64
|
+
observability?: boolean | DocsObservabilityConfig;
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
67
|
* Create a unified docs API route handler.
|
package/dist/docs-api.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { getNextAppDir } from "./get-app-dir.mjs";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
|
-
import { emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
|
|
6
|
+
import { createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
|
|
7
7
|
import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource, resolveDocsMcpConfig } from "@farming-labs/docs/server";
|
|
8
8
|
|
|
9
9
|
//#region src/docs-api.ts
|
|
@@ -660,6 +660,45 @@ function resolveAgentMdxContent(content, audience) {
|
|
|
660
660
|
function stripMdx(content) {
|
|
661
661
|
return content.replace(/^(import|export)\s.*$/gm, "").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
662
662
|
}
|
|
663
|
+
function resolveDocsSearchPageSource(dir) {
|
|
664
|
+
return ["page.mdx", "page.md"].map((fileName) => path.join(dir, fileName)).find((candidate) => fs.existsSync(candidate));
|
|
665
|
+
}
|
|
666
|
+
function hasVisibleDescendantDocsSearchPage(dir) {
|
|
667
|
+
let entries;
|
|
668
|
+
try {
|
|
669
|
+
entries = fs.readdirSync(dir);
|
|
670
|
+
} catch {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
for (const name of entries.sort()) {
|
|
674
|
+
const full = path.join(dir, name);
|
|
675
|
+
try {
|
|
676
|
+
if (!fs.statSync(full).isDirectory()) continue;
|
|
677
|
+
} catch {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const pageSource = resolveDocsSearchPageSource(full);
|
|
681
|
+
if (pageSource) try {
|
|
682
|
+
const data = matter(fs.readFileSync(pageSource, "utf-8")).data;
|
|
683
|
+
const hiddenFolderIndex = resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden";
|
|
684
|
+
if (data.hidden !== true && !hiddenFolderIndex) return true;
|
|
685
|
+
} catch {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
if (hasVisibleDescendantDocsSearchPage(full)) return true;
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
function isHiddenFolderIndexPageDir(dir) {
|
|
693
|
+
const pageSource = resolveDocsSearchPageSource(dir);
|
|
694
|
+
if (!pageSource) return false;
|
|
695
|
+
try {
|
|
696
|
+
const data = matter(fs.readFileSync(pageSource, "utf-8")).data;
|
|
697
|
+
return resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden" && hasVisibleDescendantDocsSearchPage(dir);
|
|
698
|
+
} catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
663
702
|
function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
|
|
664
703
|
const indexes = [];
|
|
665
704
|
function isExcluded(dir) {
|
|
@@ -672,27 +711,29 @@ function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
|
|
|
672
711
|
function scan(dir, slugParts) {
|
|
673
712
|
if (!fs.existsSync(dir)) return;
|
|
674
713
|
if (isExcluded(dir)) return;
|
|
675
|
-
const
|
|
676
|
-
if (
|
|
677
|
-
const raw = fs.readFileSync(
|
|
714
|
+
const pageSource = resolveDocsSearchPageSource(dir);
|
|
715
|
+
if (pageSource) try {
|
|
716
|
+
const raw = fs.readFileSync(pageSource, "utf-8");
|
|
678
717
|
const { data } = matter(raw);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
718
|
+
if (resolvePageSidebarFolderIndexBehavior(data.sidebar) === "hidden" && hasVisibleDescendantDocsSearchPage(dir)) {} else {
|
|
719
|
+
const title = data.title || slugParts[slugParts.length - 1]?.replace(/-/g, " ") || "Documentation";
|
|
720
|
+
const description = data.description;
|
|
721
|
+
const { content: fileContent } = matter(raw);
|
|
722
|
+
const rawContent = resolveAgentMdxContent(fileContent, "human");
|
|
723
|
+
const agentRawContent = resolveAgentMdxContent(fileContent, "agent");
|
|
724
|
+
const content = stripMdx(rawContent);
|
|
725
|
+
const url = withLangInUrl(slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`, locale);
|
|
726
|
+
indexes.push({
|
|
727
|
+
title,
|
|
728
|
+
description,
|
|
729
|
+
relatedInput: data.related,
|
|
730
|
+
content,
|
|
731
|
+
rawContent,
|
|
732
|
+
agentFallbackRawContent: agentRawContent !== rawContent ? agentRawContent : void 0,
|
|
733
|
+
url,
|
|
734
|
+
locale
|
|
735
|
+
});
|
|
736
|
+
}
|
|
696
737
|
} catch {}
|
|
697
738
|
let entries;
|
|
698
739
|
try {
|
|
@@ -915,9 +956,74 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
|
|
|
915
956
|
apiKey
|
|
916
957
|
};
|
|
917
958
|
}
|
|
918
|
-
|
|
959
|
+
function safeUrlOrigin(value) {
|
|
960
|
+
try {
|
|
961
|
+
return new URL(value).origin;
|
|
962
|
+
} catch {
|
|
963
|
+
return value;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async function handleAskAI(request, indexes, aiConfig, analytics, observability, analyticsContext = {}) {
|
|
919
967
|
const url = new URL(request.url);
|
|
920
968
|
const requestStartedAt = Date.now();
|
|
969
|
+
const trace = createDocsAgentTraceContext("ask-ai");
|
|
970
|
+
const runSpanId = createDocsAgentTraceId("span");
|
|
971
|
+
const traceBase = {
|
|
972
|
+
source: "server",
|
|
973
|
+
traceId: trace.traceId,
|
|
974
|
+
url: request.url,
|
|
975
|
+
path: url.pathname,
|
|
976
|
+
locale: analyticsContext.locale
|
|
977
|
+
};
|
|
978
|
+
async function emitTrace(event) {
|
|
979
|
+
await emitDocsAgentTraceEvent(observability, {
|
|
980
|
+
...traceBase,
|
|
981
|
+
...event
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async function emitRunError(reason, outputPreview = {}) {
|
|
985
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
986
|
+
const elapsed = Math.max(0, Date.now() - requestStartedAt);
|
|
987
|
+
const common = {
|
|
988
|
+
name: "ask-ai",
|
|
989
|
+
startedAt: trace.startedAt,
|
|
990
|
+
endedAt,
|
|
991
|
+
durationMs: elapsed,
|
|
992
|
+
status: "error",
|
|
993
|
+
outputPreview: {
|
|
994
|
+
reason,
|
|
995
|
+
...outputPreview
|
|
996
|
+
},
|
|
997
|
+
metadata: { reason }
|
|
998
|
+
};
|
|
999
|
+
await emitTrace({
|
|
1000
|
+
...common,
|
|
1001
|
+
type: "error",
|
|
1002
|
+
parentSpanId: runSpanId
|
|
1003
|
+
});
|
|
1004
|
+
await emitTrace({
|
|
1005
|
+
...common,
|
|
1006
|
+
type: "run.error",
|
|
1007
|
+
spanId: runSpanId
|
|
1008
|
+
});
|
|
1009
|
+
await emitTrace({
|
|
1010
|
+
...common,
|
|
1011
|
+
type: "run.end",
|
|
1012
|
+
spanId: runSpanId
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
await emitTrace({
|
|
1016
|
+
type: "run.start",
|
|
1017
|
+
name: "ask-ai",
|
|
1018
|
+
spanId: runSpanId,
|
|
1019
|
+
startedAt: trace.startedAt,
|
|
1020
|
+
durationMs: 0,
|
|
1021
|
+
status: "started",
|
|
1022
|
+
inputPreview: {
|
|
1023
|
+
method: request.method,
|
|
1024
|
+
path: url.pathname
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
921
1027
|
let body;
|
|
922
1028
|
try {
|
|
923
1029
|
body = await request.json();
|
|
@@ -933,6 +1039,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
933
1039
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
934
1040
|
}
|
|
935
1041
|
});
|
|
1042
|
+
await emitRunError("invalid_json", { status: 400 });
|
|
936
1043
|
return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
|
|
937
1044
|
}
|
|
938
1045
|
const messages = body.messages;
|
|
@@ -948,6 +1055,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
948
1055
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
949
1056
|
}
|
|
950
1057
|
});
|
|
1058
|
+
await emitRunError("missing_messages", { status: 400 });
|
|
951
1059
|
return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
|
|
952
1060
|
}
|
|
953
1061
|
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
|
|
@@ -964,10 +1072,44 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
964
1072
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
965
1073
|
}
|
|
966
1074
|
});
|
|
1075
|
+
await emitRunError("missing_user_message", {
|
|
1076
|
+
status: 400,
|
|
1077
|
+
messageCount: messages.length
|
|
1078
|
+
});
|
|
967
1079
|
return Response.json({ error: "At least one user message is required." }, { status: 400 });
|
|
968
1080
|
}
|
|
969
1081
|
const maxResults = aiConfig.maxResults ?? 5;
|
|
970
1082
|
const query = lastUserMessage.content;
|
|
1083
|
+
await emitTrace({
|
|
1084
|
+
type: "user.input",
|
|
1085
|
+
name: "ask-ai",
|
|
1086
|
+
parentSpanId: runSpanId,
|
|
1087
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1088
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1089
|
+
durationMs: 0,
|
|
1090
|
+
status: "success",
|
|
1091
|
+
inputPreview: {
|
|
1092
|
+
messageCount: messages.length,
|
|
1093
|
+
questionLength: query.length,
|
|
1094
|
+
requestedModel: typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
const retrievalStartedAt = Date.now();
|
|
1098
|
+
const retrievalStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1099
|
+
const retrievalSpanId = createDocsAgentTraceId("span");
|
|
1100
|
+
await emitTrace({
|
|
1101
|
+
type: "retrieval.query",
|
|
1102
|
+
name: "docs-index",
|
|
1103
|
+
spanId: retrievalSpanId,
|
|
1104
|
+
parentSpanId: runSpanId,
|
|
1105
|
+
startedAt: retrievalStartedAtIso,
|
|
1106
|
+
status: "started",
|
|
1107
|
+
inputPreview: {
|
|
1108
|
+
queryLength: query.length,
|
|
1109
|
+
maxResults,
|
|
1110
|
+
indexSize: indexes.length
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
971
1113
|
const scored = indexes.map((doc) => {
|
|
972
1114
|
const q = query.toLowerCase();
|
|
973
1115
|
const titleMatch = doc.title.toLowerCase().includes(q) ? 10 : 0;
|
|
@@ -979,12 +1121,52 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
979
1121
|
score: titleMatch + contentMatch
|
|
980
1122
|
};
|
|
981
1123
|
}).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
1124
|
+
await emitTrace({
|
|
1125
|
+
type: "retrieval.result",
|
|
1126
|
+
name: "docs-index",
|
|
1127
|
+
parentSpanId: retrievalSpanId,
|
|
1128
|
+
startedAt: retrievalStartedAtIso,
|
|
1129
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1130
|
+
durationMs: Math.max(0, Date.now() - retrievalStartedAt),
|
|
1131
|
+
status: "success",
|
|
1132
|
+
outputPreview: {
|
|
1133
|
+
resultCount: scored.length,
|
|
1134
|
+
urls: scored.slice(0, 5).map((doc) => doc.url)
|
|
1135
|
+
},
|
|
1136
|
+
metadata: {
|
|
1137
|
+
maxResults,
|
|
1138
|
+
indexSize: indexes.length
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
const promptStartedAt = Date.now();
|
|
1142
|
+
const promptStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1143
|
+
const promptSpanId = createDocsAgentTraceId("span");
|
|
982
1144
|
const context = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`).join("\n\n---\n\n");
|
|
983
1145
|
const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
984
|
-
const
|
|
1146
|
+
const systemMessage = {
|
|
985
1147
|
role: "system",
|
|
986
1148
|
content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
|
|
987
|
-
}
|
|
1149
|
+
};
|
|
1150
|
+
const llmMessages = [systemMessage, ...messages.filter((m) => m.role !== "system")];
|
|
1151
|
+
await emitTrace({
|
|
1152
|
+
type: "prompt.build",
|
|
1153
|
+
name: "ask-ai.prompt",
|
|
1154
|
+
spanId: promptSpanId,
|
|
1155
|
+
parentSpanId: runSpanId,
|
|
1156
|
+
startedAt: promptStartedAtIso,
|
|
1157
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1158
|
+
durationMs: Math.max(0, Date.now() - promptStartedAt),
|
|
1159
|
+
status: "success",
|
|
1160
|
+
inputPreview: {
|
|
1161
|
+
messageCount: messages.length,
|
|
1162
|
+
retrievedCount: scored.length
|
|
1163
|
+
},
|
|
1164
|
+
outputPreview: {
|
|
1165
|
+
llmMessageCount: llmMessages.length,
|
|
1166
|
+
contextChars: context.length,
|
|
1167
|
+
systemMessageChars: systemMessage.content.length
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
988
1170
|
const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
|
|
989
1171
|
if (!resolved.apiKey) {
|
|
990
1172
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1003,6 +1185,13 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1003
1185
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1004
1186
|
}
|
|
1005
1187
|
});
|
|
1188
|
+
await emitRunError("missing_api_key", {
|
|
1189
|
+
status: 500,
|
|
1190
|
+
messageCount: messages.length,
|
|
1191
|
+
questionLength: query.length,
|
|
1192
|
+
retrievedCount: scored.length,
|
|
1193
|
+
model: resolved.model
|
|
1194
|
+
});
|
|
1006
1195
|
return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config ai section, configure a provider, or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
|
|
1007
1196
|
}
|
|
1008
1197
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1019,20 +1208,100 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1019
1208
|
model: resolved.model
|
|
1020
1209
|
}
|
|
1021
1210
|
});
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1211
|
+
const modelStartedAt = Date.now();
|
|
1212
|
+
const modelStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1213
|
+
const modelSpanId = createDocsAgentTraceId("span");
|
|
1214
|
+
const providerOrigin = safeUrlOrigin(resolved.baseUrl);
|
|
1215
|
+
await emitTrace({
|
|
1216
|
+
type: "model.call",
|
|
1217
|
+
name: resolved.model,
|
|
1218
|
+
spanId: modelSpanId,
|
|
1219
|
+
parentSpanId: runSpanId,
|
|
1220
|
+
startedAt: modelStartedAtIso,
|
|
1221
|
+
status: "started",
|
|
1222
|
+
inputPreview: {
|
|
1223
|
+
messageCount: llmMessages.length,
|
|
1030
1224
|
stream: true,
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1225
|
+
providerOrigin
|
|
1226
|
+
},
|
|
1227
|
+
metadata: { model: resolved.model }
|
|
1033
1228
|
});
|
|
1229
|
+
let llmResponse;
|
|
1230
|
+
try {
|
|
1231
|
+
llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
|
|
1232
|
+
method: "POST",
|
|
1233
|
+
headers: {
|
|
1234
|
+
"Content-Type": "application/json",
|
|
1235
|
+
Authorization: `Bearer ${resolved.apiKey}`
|
|
1236
|
+
},
|
|
1237
|
+
body: JSON.stringify({
|
|
1238
|
+
model: resolved.model,
|
|
1239
|
+
stream: true,
|
|
1240
|
+
messages: llmMessages
|
|
1241
|
+
})
|
|
1242
|
+
});
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1245
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1246
|
+
await emitTrace({
|
|
1247
|
+
type: "model.error",
|
|
1248
|
+
name: resolved.model,
|
|
1249
|
+
parentSpanId: modelSpanId,
|
|
1250
|
+
startedAt: modelStartedAtIso,
|
|
1251
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1252
|
+
durationMs: elapsed,
|
|
1253
|
+
status: "error",
|
|
1254
|
+
outputPreview: { message },
|
|
1255
|
+
metadata: {
|
|
1256
|
+
model: resolved.model,
|
|
1257
|
+
providerOrigin
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
await emitDocsAnalyticsEvent(analytics, {
|
|
1261
|
+
type: "api_ai_error",
|
|
1262
|
+
source: "server",
|
|
1263
|
+
url: request.url,
|
|
1264
|
+
path: url.pathname,
|
|
1265
|
+
locale: analyticsContext.locale,
|
|
1266
|
+
input: { question: query },
|
|
1267
|
+
properties: {
|
|
1268
|
+
reason: "llm_fetch_error",
|
|
1269
|
+
messageCount: messages.length,
|
|
1270
|
+
questionLength: query.length,
|
|
1271
|
+
retrievedCount: scored.length,
|
|
1272
|
+
model: resolved.model,
|
|
1273
|
+
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
await emitRunError("llm_fetch_error", {
|
|
1277
|
+
status: 502,
|
|
1278
|
+
messageCount: messages.length,
|
|
1279
|
+
questionLength: query.length,
|
|
1280
|
+
retrievedCount: scored.length,
|
|
1281
|
+
model: resolved.model
|
|
1282
|
+
});
|
|
1283
|
+
return Response.json({ error: "LLM API request failed." }, { status: 502 });
|
|
1284
|
+
}
|
|
1034
1285
|
if (!llmResponse.ok) {
|
|
1035
1286
|
const errText = await llmResponse.text().catch(() => "Unknown error");
|
|
1287
|
+
const elapsed = Math.max(0, Date.now() - modelStartedAt);
|
|
1288
|
+
await emitTrace({
|
|
1289
|
+
type: "model.error",
|
|
1290
|
+
name: resolved.model,
|
|
1291
|
+
parentSpanId: modelSpanId,
|
|
1292
|
+
startedAt: modelStartedAtIso,
|
|
1293
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1294
|
+
durationMs: elapsed,
|
|
1295
|
+
status: "error",
|
|
1296
|
+
outputPreview: {
|
|
1297
|
+
status: llmResponse.status,
|
|
1298
|
+
errorChars: errText.length
|
|
1299
|
+
},
|
|
1300
|
+
metadata: {
|
|
1301
|
+
model: resolved.model,
|
|
1302
|
+
providerOrigin
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1036
1305
|
await emitDocsAnalyticsEvent(analytics, {
|
|
1037
1306
|
type: "api_ai_error",
|
|
1038
1307
|
source: "server",
|
|
@@ -1050,6 +1319,14 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1050
1319
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1051
1320
|
}
|
|
1052
1321
|
});
|
|
1322
|
+
await emitRunError("llm_error", {
|
|
1323
|
+
status: 502,
|
|
1324
|
+
modelStatus: llmResponse.status,
|
|
1325
|
+
messageCount: messages.length,
|
|
1326
|
+
questionLength: query.length,
|
|
1327
|
+
retrievedCount: scored.length,
|
|
1328
|
+
model: resolved.model
|
|
1329
|
+
});
|
|
1053
1330
|
return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
|
|
1054
1331
|
}
|
|
1055
1332
|
await emitDocsAnalyticsEvent(analytics, {
|
|
@@ -1067,6 +1344,66 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
|
|
|
1067
1344
|
durationMs: Math.max(0, Date.now() - requestStartedAt)
|
|
1068
1345
|
}
|
|
1069
1346
|
});
|
|
1347
|
+
const responseEndedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1348
|
+
const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
|
|
1349
|
+
await emitTrace({
|
|
1350
|
+
type: "model.response",
|
|
1351
|
+
name: resolved.model,
|
|
1352
|
+
parentSpanId: modelSpanId,
|
|
1353
|
+
startedAt: modelStartedAtIso,
|
|
1354
|
+
endedAt: responseEndedAt,
|
|
1355
|
+
durationMs: modelDurationMs,
|
|
1356
|
+
status: "success",
|
|
1357
|
+
outputPreview: {
|
|
1358
|
+
status: llmResponse.status,
|
|
1359
|
+
stream: true,
|
|
1360
|
+
contentType: llmResponse.headers.get("content-type") ?? void 0
|
|
1361
|
+
},
|
|
1362
|
+
metadata: {
|
|
1363
|
+
model: resolved.model,
|
|
1364
|
+
providerOrigin
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
await emitTrace({
|
|
1368
|
+
type: "model.stream",
|
|
1369
|
+
name: resolved.model,
|
|
1370
|
+
parentSpanId: modelSpanId,
|
|
1371
|
+
startedAt: modelStartedAtIso,
|
|
1372
|
+
endedAt: responseEndedAt,
|
|
1373
|
+
durationMs: modelDurationMs,
|
|
1374
|
+
status: "success",
|
|
1375
|
+
outputPreview: { stream: true },
|
|
1376
|
+
metadata: { model: resolved.model }
|
|
1377
|
+
});
|
|
1378
|
+
const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
|
|
1379
|
+
await emitTrace({
|
|
1380
|
+
type: "agent.final",
|
|
1381
|
+
name: "ask-ai",
|
|
1382
|
+
parentSpanId: runSpanId,
|
|
1383
|
+
startedAt: trace.startedAt,
|
|
1384
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1385
|
+
durationMs: runDurationMs,
|
|
1386
|
+
status: "success",
|
|
1387
|
+
outputPreview: {
|
|
1388
|
+
stream: true,
|
|
1389
|
+
retrievedCount: scored.length
|
|
1390
|
+
},
|
|
1391
|
+
metadata: { model: resolved.model }
|
|
1392
|
+
});
|
|
1393
|
+
await emitTrace({
|
|
1394
|
+
type: "run.end",
|
|
1395
|
+
name: "ask-ai",
|
|
1396
|
+
spanId: runSpanId,
|
|
1397
|
+
startedAt: trace.startedAt,
|
|
1398
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1399
|
+
durationMs: runDurationMs,
|
|
1400
|
+
status: "success",
|
|
1401
|
+
outputPreview: {
|
|
1402
|
+
stream: true,
|
|
1403
|
+
retrievedCount: scored.length
|
|
1404
|
+
},
|
|
1405
|
+
metadata: { model: resolved.model }
|
|
1406
|
+
});
|
|
1070
1407
|
return new Response(llmResponse.body, { headers: {
|
|
1071
1408
|
"Content-Type": "text/event-stream",
|
|
1072
1409
|
"Cache-Control": "no-cache",
|
|
@@ -1147,6 +1484,7 @@ function createDocsAPI(options) {
|
|
|
1147
1484
|
const root = options?.rootDir ?? process.cwd();
|
|
1148
1485
|
const entry = options?.entry ?? readEntry(root);
|
|
1149
1486
|
const analytics = options?.analytics;
|
|
1487
|
+
const observability = options?.observability;
|
|
1150
1488
|
const appDir = getNextAppDir(root);
|
|
1151
1489
|
const contentDir = options?.contentDir ?? path.join(appDir, entry);
|
|
1152
1490
|
const changelogConfig = resolveChangelogConfig(options?.changelog);
|
|
@@ -1249,11 +1587,14 @@ function createDocsAPI(options) {
|
|
|
1249
1587
|
return sources;
|
|
1250
1588
|
}
|
|
1251
1589
|
async function getMarkdownDocument(ctx, requestedPath) {
|
|
1590
|
+
const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
|
|
1591
|
+
const normalizedEntry = `/${normalizePathSegment(ctx.entryPath)}`;
|
|
1592
|
+
const relativeSlug = normalizedRequest === normalizedEntry ? "" : normalizedRequest.slice(normalizedEntry.length).replace(/^\/+/, "");
|
|
1593
|
+
for (const docsDir of ctx.docsDirs) if (isHiddenFolderIndexPageDir(relativeSlug ? path.join(docsDir, ...relativeSlug.split("/")) : docsDir)) return null;
|
|
1252
1594
|
for (const source of getMarkdownSources(ctx)) {
|
|
1253
1595
|
const page = findDocsMcpPage(ctx.entryPath, await source.getPages(), requestedPath);
|
|
1254
1596
|
if (page) return renderMarkdownDocument(page);
|
|
1255
1597
|
}
|
|
1256
|
-
const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
|
|
1257
1598
|
const fallbackPage = getIndexes(ctx).find((page) => normalizeUrlPath(page.url) === normalizedRequest);
|
|
1258
1599
|
if (fallbackPage) return renderMarkdownDocument(fallbackPage);
|
|
1259
1600
|
for (const page of getIndexes(ctx)) if (normalizePathSegment(page.url.replace(/^\/+/, "").replace(`${ctx.entryPath}/`, "")) === normalizePathSegment(requestedPath.replace(/^\/+/, "").replace(/\.md$/i, ""))) return renderMarkdownDocument(page);
|
|
@@ -1542,7 +1883,7 @@ function createDocsAPI(options) {
|
|
|
1542
1883
|
return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
|
|
1543
1884
|
}
|
|
1544
1885
|
const ctx = resolveContextFromRequest(request);
|
|
1545
|
-
return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, { locale: ctx.locale });
|
|
1886
|
+
return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, observability, { locale: ctx.locale });
|
|
1546
1887
|
}
|
|
1547
1888
|
};
|
|
1548
1889
|
}
|
|
@@ -1572,6 +1913,7 @@ function createDocsMCPAPI(options = {}) {
|
|
|
1572
1913
|
mcp: options.mcp ?? readMcpConfig(rootDir),
|
|
1573
1914
|
search: options.search,
|
|
1574
1915
|
analytics: options.analytics,
|
|
1916
|
+
observability: options.observability,
|
|
1575
1917
|
defaultName: navTitle
|
|
1576
1918
|
});
|
|
1577
1919
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.72",
|
|
4
4
|
"description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"tsdown": "^0.20.3",
|
|
140
140
|
"typescript": "^5.9.3",
|
|
141
141
|
"vitest": "^3.2.4",
|
|
142
|
-
"@farming-labs/docs": "0.1.
|
|
142
|
+
"@farming-labs/docs": "0.1.72"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
145
|
"@farming-labs/docs": ">=0.0.1",
|