@farming-labs/svelte 0.1.71 → 0.1.73

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.
Files changed (2) hide show
  1. package/dist/server.js +314 -28
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -30,7 +30,7 @@
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
32
  import matter from "gray-matter";
33
- import { applySidebarFolderIndexBehavior, buildDocsAgentDiscoverySpec, emitDocsAnalyticsEvent, findDocsMarkdownPage, isDocsAgentDiscoveryRequest, isDocsSkillRequest, normalizeDocsRelated, performDocsSearch, renderDocsMarkdownDocument, renderDocsSkillDocument, stripGeneratedAgentProvenance, resolveDocsAgentMdxContent, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, resolveDocsI18n, resolveDocsLlmsTxtFormat, resolveDocsLocale, resolveDocsMarkdownRequest, resolveDocsPath, resolvePageReadingTime, resolveReadingTimeOptions, resolveDocsSkillFormat, } from "@farming-labs/docs";
33
+ import { applySidebarFolderIndexBehavior, buildDocsAskAIContext, buildDocsAgentDiscoverySpec, createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, formatDocsAskAIPackageHints, findDocsMarkdownPage, isDocsAgentDiscoveryRequest, isDocsSkillRequest, normalizeDocsRelated, performDocsSearch, renderDocsMarkdownDocument, renderDocsSkillDocument, stripGeneratedAgentProvenance, resolveDocsAgentMdxContent, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, resolveDocsI18n, resolveDocsLlmsTxtFormat, resolveDocsLocale, resolveDocsMarkdownRequest, resolveDocsPath, resolvePageReadingTime, resolveReadingTimeOptions, resolveDocsSkillFormat, } from "@farming-labs/docs";
34
34
  import { createDocsMcpHttpHandler, resolveDocsMcpConfig, serializeDocsIconRegistry, serializeOpenDocsProviders, } from "@farming-labs/docs/server";
35
35
  import { loadDocsNavTree, loadDocsContent, flattenNavTree } from "./content.js";
36
36
  import { renderMarkdown } from "./markdown.js";
@@ -59,6 +59,14 @@ function resolveAIModelAndProvider(aiConfig, requestedModelId) {
59
59
  (typeof process !== "undefined" ? process.env?.OPENAI_API_KEY : undefined);
60
60
  return { model: modelId, baseUrl, apiKey };
61
61
  }
62
+ function safeUrlOrigin(value) {
63
+ try {
64
+ return new URL(value).origin;
65
+ }
66
+ catch {
67
+ return value;
68
+ }
69
+ }
62
70
  function stripMarkdownText(content) {
63
71
  return content
64
72
  .replace(/^(import|export)\s.*$/gm, "")
@@ -342,6 +350,7 @@ function findPageInMap(contentMap, dirPrefix, slug) {
342
350
  export function createDocsServer(config = {}) {
343
351
  const entry = config.entry ?? "docs";
344
352
  const analytics = config.analytics;
353
+ const observability = config.observability;
345
354
  const contentDirBase = config.contentDir ?? entry;
346
355
  const rootDir = path.resolve(config.rootDir ?? process.cwd());
347
356
  const i18n = resolveDocsI18n(config.i18n);
@@ -539,20 +548,6 @@ export function createDocsServer(config = {}) {
539
548
  searchIndexByEntry.set(key, index);
540
549
  return index;
541
550
  }
542
- function searchByQuery(query, ctx) {
543
- const index = getSearchIndex(ctx);
544
- return index
545
- .map((page) => {
546
- const titleMatch = page.title.toLowerCase().includes(query) ? 10 : 0;
547
- const words = query.split(/\s+/);
548
- const contentMatch = words.reduce((score, word) => {
549
- return score + (page.content.toLowerCase().includes(word) ? 1 : 0);
550
- }, 0);
551
- return { ...page, score: titleMatch + contentMatch };
552
- })
553
- .filter((r) => r.score > 0)
554
- .sort((a, b) => b.score - a.score);
555
- }
556
551
  // ─── llms.txt content builder ────────────────────────────────
557
552
  const llmsSiteTitle = typeof config.nav === "object" &&
558
553
  typeof config.nav?.title === "string"
@@ -739,12 +734,16 @@ export function createDocsServer(config = {}) {
739
734
  function buildDefaultSystemPrompt() {
740
735
  const lines = [
741
736
  `You are a helpful documentation assistant${projectName ? ` for ${projectName}` : ""}.`,
742
- "Answer questions based on the provided documentation context.",
737
+ "Answer only from the provided documentation context.",
738
+ "Prefer exact code/config snippets from the context when the question asks how to implement something.",
739
+ "Cite the relevant documentation URL when you use a source.",
740
+ "Use only URLs exactly as they appear in the context; do not invent placeholder domains.",
741
+ 'Never use placeholder package names or imports such as "your-auth-library", "your-package", "your-sdk", "replace-me", or "example-library". If the exact package or import is not in the context, do not include an import snippet.',
743
742
  "Be concise and accurate. If the answer is not in the context, say so honestly.",
744
743
  "Use markdown formatting for code examples and links.",
745
744
  ];
746
745
  if (packageName) {
747
- lines.push(`When showing import examples, always use "${packageName}" as the package name.`);
746
+ lines.push(`When showing import examples, use "${packageName}" as the package name and prefer exact imports copied from the documentation context.`);
748
747
  }
749
748
  if (docsUrl) {
750
749
  lines.push(`When linking to documentation pages, use "${docsUrl}" as the base URL (e.g. ${docsUrl}/docs/get-started).`);
@@ -755,6 +754,51 @@ export function createDocsServer(config = {}) {
755
754
  async function POST(event) {
756
755
  const requestUrl = new URL(event.request.url);
757
756
  const requestStartedAt = Date.now();
757
+ const trace = createDocsAgentTraceContext("ask-ai");
758
+ const runSpanId = createDocsAgentTraceId("span");
759
+ const traceBase = {
760
+ source: "server",
761
+ traceId: trace.traceId,
762
+ url: event.request.url,
763
+ path: requestUrl.pathname,
764
+ };
765
+ async function emitTrace(traceEvent) {
766
+ await emitDocsAgentTraceEvent(observability, {
767
+ ...traceBase,
768
+ ...traceEvent,
769
+ });
770
+ }
771
+ async function emitRunError(reason, outputPreview = {}) {
772
+ const endedAt = new Date().toISOString();
773
+ const elapsed = Math.max(0, Date.now() - requestStartedAt);
774
+ const common = {
775
+ name: "ask-ai",
776
+ startedAt: trace.startedAt,
777
+ endedAt,
778
+ durationMs: elapsed,
779
+ status: "error",
780
+ outputPreview: {
781
+ reason,
782
+ ...outputPreview,
783
+ },
784
+ metadata: { reason },
785
+ };
786
+ await emitTrace({ ...common, type: "error", parentSpanId: runSpanId });
787
+ await emitTrace({ ...common, type: "run.error", spanId: runSpanId });
788
+ await emitTrace({ ...common, type: "run.end", spanId: runSpanId });
789
+ }
790
+ await emitTrace({
791
+ type: "run.start",
792
+ name: "ask-ai",
793
+ spanId: runSpanId,
794
+ startedAt: trace.startedAt,
795
+ durationMs: 0,
796
+ status: "started",
797
+ inputPreview: {
798
+ method: event.request.method,
799
+ path: requestUrl.pathname,
800
+ },
801
+ });
758
802
  if (!aiConfig.enabled) {
759
803
  await emitDocsAnalyticsEvent(analytics, {
760
804
  type: "api_ai_error",
@@ -763,6 +807,7 @@ export function createDocsServer(config = {}) {
763
807
  path: requestUrl.pathname,
764
808
  properties: { reason: "disabled" },
765
809
  });
810
+ await emitRunError("disabled", { status: 404 });
766
811
  return new Response(JSON.stringify({
767
812
  error: "AI is not enabled. Set `ai: { enabled: true }` in your docs config to enable it.",
768
813
  }), { status: 404, headers: { "Content-Type": "application/json" } });
@@ -781,6 +826,7 @@ export function createDocsServer(config = {}) {
781
826
  durationMs: Math.max(0, Date.now() - requestStartedAt),
782
827
  },
783
828
  });
829
+ await emitRunError("missing_api_key", { status: 500 });
784
830
  return new Response(JSON.stringify({
785
831
  error: "AI is enabled but no API key was found. Set `apiKey` in your docs config `ai` section or add OPENAI_API_KEY to your environment.",
786
832
  }), { status: 500, headers: { "Content-Type": "application/json" } });
@@ -802,6 +848,7 @@ export function createDocsServer(config = {}) {
802
848
  durationMs: Math.max(0, Date.now() - requestStartedAt),
803
849
  },
804
850
  });
851
+ await emitRunError("invalid_json", { status: 400, locale: ctx.locale });
805
852
  return new Response(JSON.stringify({ error: "Invalid JSON body. Expected { messages: [...] }" }), { status: 400, headers: { "Content-Type": "application/json" } });
806
853
  }
807
854
  const messages = body.messages;
@@ -817,6 +864,7 @@ export function createDocsServer(config = {}) {
817
864
  durationMs: Math.max(0, Date.now() - requestStartedAt),
818
865
  },
819
866
  });
867
+ await emitRunError("missing_messages", { status: 400, locale: ctx.locale });
820
868
  return new Response(JSON.stringify({ error: "messages array is required and must not be empty." }), { status: 400, headers: { "Content-Type": "application/json" } });
821
869
  }
822
870
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
@@ -833,26 +881,113 @@ export function createDocsServer(config = {}) {
833
881
  durationMs: Math.max(0, Date.now() - requestStartedAt),
834
882
  },
835
883
  });
884
+ await emitRunError("missing_user_message", {
885
+ status: 400,
886
+ locale: ctx.locale,
887
+ messageCount: messages.length,
888
+ });
836
889
  return new Response(JSON.stringify({ error: "At least one user message is required." }), {
837
890
  status: 400,
838
891
  headers: { "Content-Type": "application/json" },
839
892
  });
840
893
  }
841
894
  const maxResults = aiConfig.maxResults ?? 5;
842
- const scored = searchByQuery(lastUserMessage.content.toLowerCase(), ctx).slice(0, maxResults);
843
- const contextParts = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`);
844
- const context = contextParts.join("\n\n---\n\n");
895
+ await emitTrace({
896
+ type: "user.input",
897
+ name: "ask-ai",
898
+ parentSpanId: runSpanId,
899
+ startedAt: new Date().toISOString(),
900
+ endedAt: new Date().toISOString(),
901
+ durationMs: 0,
902
+ status: "success",
903
+ locale: ctx.locale,
904
+ inputPreview: {
905
+ messageCount: messages.length,
906
+ questionLength: lastUserMessage.content.length,
907
+ requestedModel: typeof body.model === "string" && body.model.trim().length > 0
908
+ ? body.model.trim()
909
+ : undefined,
910
+ },
911
+ });
912
+ const retrievalStartedAt = Date.now();
913
+ const retrievalStartedAtIso = new Date().toISOString();
914
+ const retrievalSpanId = createDocsAgentTraceId("span");
915
+ await emitTrace({
916
+ type: "retrieval.query",
917
+ name: "docs-index",
918
+ spanId: retrievalSpanId,
919
+ parentSpanId: runSpanId,
920
+ startedAt: retrievalStartedAtIso,
921
+ status: "started",
922
+ locale: ctx.locale,
923
+ inputPreview: {
924
+ queryLength: lastUserMessage.content.length,
925
+ maxResults,
926
+ },
927
+ });
928
+ const retrieval = await buildDocsAskAIContext({
929
+ pages: getSearchIndex(ctx),
930
+ query: lastUserMessage.content,
931
+ search: resolveSearchRequestConfig(config.search, event.request.url),
932
+ locale: ctx.locale,
933
+ pathname: requestUrl.searchParams.get("pathname") ?? undefined,
934
+ siteTitle: llmsTitle,
935
+ baseUrl: requestUrl.origin,
936
+ limit: maxResults,
937
+ });
938
+ const scored = retrieval.results;
939
+ await emitTrace({
940
+ type: "retrieval.result",
941
+ name: "docs-index",
942
+ parentSpanId: retrievalSpanId,
943
+ startedAt: retrievalStartedAtIso,
944
+ endedAt: new Date().toISOString(),
945
+ durationMs: Math.max(0, Date.now() - retrievalStartedAt),
946
+ status: "success",
947
+ locale: ctx.locale,
948
+ outputPreview: {
949
+ resultCount: scored.length,
950
+ urls: scored.slice(0, 5).map((doc) => doc.url),
951
+ },
952
+ metadata: { maxResults },
953
+ });
954
+ const promptStartedAt = Date.now();
955
+ const promptStartedAtIso = new Date().toISOString();
956
+ const promptSpanId = createDocsAgentTraceId("span");
957
+ const context = retrieval.context;
845
958
  const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
959
+ const packageHintsPrompt = formatDocsAskAIPackageHints(retrieval.packageHints, packageName);
960
+ const fullSystemPrompt = [systemPrompt, packageHintsPrompt].filter(Boolean).join("\n\n");
846
961
  const systemMessage = {
847
962
  role: "system",
848
963
  content: context
849
- ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}`
850
- : systemPrompt,
964
+ ? `${fullSystemPrompt}\n\n---\n\nDocumentation context:\n\n${context}`
965
+ : fullSystemPrompt,
851
966
  };
852
967
  const llmMessages = [
853
968
  systemMessage,
854
969
  ...messages.filter((m) => m.role !== "system"),
855
970
  ];
971
+ await emitTrace({
972
+ type: "prompt.build",
973
+ name: "ask-ai.prompt",
974
+ spanId: promptSpanId,
975
+ parentSpanId: runSpanId,
976
+ startedAt: promptStartedAtIso,
977
+ endedAt: new Date().toISOString(),
978
+ durationMs: Math.max(0, Date.now() - promptStartedAt),
979
+ status: "success",
980
+ locale: ctx.locale,
981
+ inputPreview: {
982
+ messageCount: messages.length,
983
+ retrievedCount: scored.length,
984
+ },
985
+ outputPreview: {
986
+ llmMessageCount: llmMessages.length,
987
+ contextChars: context.length,
988
+ systemMessageChars: systemMessage.content.length,
989
+ },
990
+ });
856
991
  const requestedModel = typeof body.model === "string" && body.model.trim().length > 0
857
992
  ? body.model.trim()
858
993
  : undefined;
@@ -872,16 +1007,96 @@ export function createDocsServer(config = {}) {
872
1007
  model: resolved.model,
873
1008
  },
874
1009
  });
875
- const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
876
- method: "POST",
877
- headers: {
878
- "Content-Type": "application/json",
879
- Authorization: `Bearer ${finalKey}`,
1010
+ const modelStartedAt = Date.now();
1011
+ const modelStartedAtIso = new Date().toISOString();
1012
+ const modelSpanId = createDocsAgentTraceId("span");
1013
+ const providerOrigin = safeUrlOrigin(resolved.baseUrl);
1014
+ await emitTrace({
1015
+ type: "model.call",
1016
+ name: resolved.model,
1017
+ spanId: modelSpanId,
1018
+ parentSpanId: runSpanId,
1019
+ startedAt: modelStartedAtIso,
1020
+ status: "started",
1021
+ locale: ctx.locale,
1022
+ inputPreview: {
1023
+ messageCount: llmMessages.length,
1024
+ stream: true,
1025
+ providerOrigin,
880
1026
  },
881
- body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
1027
+ metadata: { model: resolved.model },
882
1028
  });
1029
+ let llmResponse;
1030
+ try {
1031
+ llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1032
+ method: "POST",
1033
+ headers: {
1034
+ "Content-Type": "application/json",
1035
+ Authorization: `Bearer ${finalKey}`,
1036
+ },
1037
+ body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
1038
+ });
1039
+ }
1040
+ catch (error) {
1041
+ const message = error instanceof Error ? error.message : "Unknown error";
1042
+ await emitTrace({
1043
+ type: "model.error",
1044
+ name: resolved.model,
1045
+ parentSpanId: modelSpanId,
1046
+ startedAt: modelStartedAtIso,
1047
+ endedAt: new Date().toISOString(),
1048
+ durationMs: Math.max(0, Date.now() - modelStartedAt),
1049
+ status: "error",
1050
+ locale: ctx.locale,
1051
+ outputPreview: { message },
1052
+ metadata: { model: resolved.model, providerOrigin },
1053
+ });
1054
+ await emitDocsAnalyticsEvent(analytics, {
1055
+ type: "api_ai_error",
1056
+ source: "server",
1057
+ url: event.request.url,
1058
+ path: requestUrl.pathname,
1059
+ locale: ctx.locale,
1060
+ input: { question: lastUserMessage.content },
1061
+ properties: {
1062
+ reason: "llm_fetch_error",
1063
+ messageCount: messages.length,
1064
+ questionLength: lastUserMessage.content.length,
1065
+ retrievedCount: scored.length,
1066
+ model: resolved.model,
1067
+ durationMs: Math.max(0, Date.now() - requestStartedAt),
1068
+ },
1069
+ });
1070
+ await emitRunError("llm_fetch_error", {
1071
+ status: 502,
1072
+ locale: ctx.locale,
1073
+ messageCount: messages.length,
1074
+ questionLength: lastUserMessage.content.length,
1075
+ retrievedCount: scored.length,
1076
+ model: resolved.model,
1077
+ });
1078
+ return new Response(JSON.stringify({ error: "LLM API request failed." }), {
1079
+ status: 502,
1080
+ headers: { "Content-Type": "application/json" },
1081
+ });
1082
+ }
883
1083
  if (!llmResponse.ok) {
884
1084
  const errText = await llmResponse.text().catch(() => "Unknown error");
1085
+ await emitTrace({
1086
+ type: "model.error",
1087
+ name: resolved.model,
1088
+ parentSpanId: modelSpanId,
1089
+ startedAt: modelStartedAtIso,
1090
+ endedAt: new Date().toISOString(),
1091
+ durationMs: Math.max(0, Date.now() - modelStartedAt),
1092
+ status: "error",
1093
+ locale: ctx.locale,
1094
+ outputPreview: {
1095
+ status: llmResponse.status,
1096
+ errorChars: errText.length,
1097
+ },
1098
+ metadata: { model: resolved.model, providerOrigin },
1099
+ });
885
1100
  await emitDocsAnalyticsEvent(analytics, {
886
1101
  type: "api_ai_error",
887
1102
  source: "server",
@@ -899,6 +1114,15 @@ export function createDocsServer(config = {}) {
899
1114
  durationMs: Math.max(0, Date.now() - requestStartedAt),
900
1115
  },
901
1116
  });
1117
+ await emitRunError("llm_error", {
1118
+ status: 502,
1119
+ modelStatus: llmResponse.status,
1120
+ locale: ctx.locale,
1121
+ messageCount: messages.length,
1122
+ questionLength: lastUserMessage.content.length,
1123
+ retrievedCount: scored.length,
1124
+ model: resolved.model,
1125
+ });
902
1126
  return new Response(JSON.stringify({ error: `LLM API error (${llmResponse.status}): ${errText}` }), { status: 502, headers: { "Content-Type": "application/json" } });
903
1127
  }
904
1128
  await emitDocsAnalyticsEvent(analytics, {
@@ -916,6 +1140,67 @@ export function createDocsServer(config = {}) {
916
1140
  durationMs: Math.max(0, Date.now() - requestStartedAt),
917
1141
  },
918
1142
  });
1143
+ const responseEndedAt = new Date().toISOString();
1144
+ const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
1145
+ await emitTrace({
1146
+ type: "model.response",
1147
+ name: resolved.model,
1148
+ parentSpanId: modelSpanId,
1149
+ startedAt: modelStartedAtIso,
1150
+ endedAt: responseEndedAt,
1151
+ durationMs: modelDurationMs,
1152
+ status: "success",
1153
+ locale: ctx.locale,
1154
+ outputPreview: {
1155
+ status: llmResponse.status,
1156
+ stream: true,
1157
+ contentType: llmResponse.headers.get("content-type") ?? undefined,
1158
+ },
1159
+ metadata: { model: resolved.model, providerOrigin },
1160
+ });
1161
+ await emitTrace({
1162
+ type: "model.stream",
1163
+ name: resolved.model,
1164
+ parentSpanId: modelSpanId,
1165
+ startedAt: modelStartedAtIso,
1166
+ endedAt: responseEndedAt,
1167
+ durationMs: modelDurationMs,
1168
+ status: "success",
1169
+ locale: ctx.locale,
1170
+ outputPreview: { stream: true },
1171
+ metadata: { model: resolved.model },
1172
+ });
1173
+ const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
1174
+ await emitTrace({
1175
+ type: "agent.final",
1176
+ name: "ask-ai",
1177
+ parentSpanId: runSpanId,
1178
+ startedAt: trace.startedAt,
1179
+ endedAt: new Date().toISOString(),
1180
+ durationMs: runDurationMs,
1181
+ status: "success",
1182
+ locale: ctx.locale,
1183
+ outputPreview: {
1184
+ stream: true,
1185
+ retrievedCount: scored.length,
1186
+ },
1187
+ metadata: { model: resolved.model },
1188
+ });
1189
+ await emitTrace({
1190
+ type: "run.end",
1191
+ name: "ask-ai",
1192
+ spanId: runSpanId,
1193
+ startedAt: trace.startedAt,
1194
+ endedAt: new Date().toISOString(),
1195
+ durationMs: runDurationMs,
1196
+ status: "success",
1197
+ locale: ctx.locale,
1198
+ outputPreview: {
1199
+ stream: true,
1200
+ retrievedCount: scored.length,
1201
+ },
1202
+ metadata: { model: resolved.model },
1203
+ });
919
1204
  return new Response(llmResponse.body, {
920
1205
  headers: {
921
1206
  "Content-Type": "text/event-stream",
@@ -947,6 +1232,7 @@ export function createDocsServer(config = {}) {
947
1232
  },
948
1233
  mcp: config.mcp,
949
1234
  analytics,
1235
+ observability,
950
1236
  defaultName: mcpSiteTitle,
951
1237
  });
952
1238
  return { load, GET, POST, MCP };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/svelte",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "SvelteKit adapter for @farming-labs/docs — content loading and navigation utilities",
5
5
  "keywords": [
6
6
  "docs",
@@ -56,7 +56,7 @@
56
56
  "devDependencies": {
57
57
  "@types/node": "^22.10.0",
58
58
  "typescript": "^5.9.3",
59
- "@farming-labs/docs": "0.1.71"
59
+ "@farming-labs/docs": "0.1.73"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@farming-labs/docs": "*"