@farming-labs/svelte 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.
Files changed (2) hide show
  1. package/dist/server.js +292 -7
  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, buildDocsAgentDiscoverySpec, createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, 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);
@@ -755,6 +764,51 @@ export function createDocsServer(config = {}) {
755
764
  async function POST(event) {
756
765
  const requestUrl = new URL(event.request.url);
757
766
  const requestStartedAt = Date.now();
767
+ const trace = createDocsAgentTraceContext("ask-ai");
768
+ const runSpanId = createDocsAgentTraceId("span");
769
+ const traceBase = {
770
+ source: "server",
771
+ traceId: trace.traceId,
772
+ url: event.request.url,
773
+ path: requestUrl.pathname,
774
+ };
775
+ async function emitTrace(traceEvent) {
776
+ await emitDocsAgentTraceEvent(observability, {
777
+ ...traceBase,
778
+ ...traceEvent,
779
+ });
780
+ }
781
+ async function emitRunError(reason, outputPreview = {}) {
782
+ const endedAt = new Date().toISOString();
783
+ const elapsed = Math.max(0, Date.now() - requestStartedAt);
784
+ const common = {
785
+ name: "ask-ai",
786
+ startedAt: trace.startedAt,
787
+ endedAt,
788
+ durationMs: elapsed,
789
+ status: "error",
790
+ outputPreview: {
791
+ reason,
792
+ ...outputPreview,
793
+ },
794
+ metadata: { reason },
795
+ };
796
+ await emitTrace({ ...common, type: "error", parentSpanId: runSpanId });
797
+ await emitTrace({ ...common, type: "run.error", spanId: runSpanId });
798
+ await emitTrace({ ...common, type: "run.end", spanId: runSpanId });
799
+ }
800
+ await emitTrace({
801
+ type: "run.start",
802
+ name: "ask-ai",
803
+ spanId: runSpanId,
804
+ startedAt: trace.startedAt,
805
+ durationMs: 0,
806
+ status: "started",
807
+ inputPreview: {
808
+ method: event.request.method,
809
+ path: requestUrl.pathname,
810
+ },
811
+ });
758
812
  if (!aiConfig.enabled) {
759
813
  await emitDocsAnalyticsEvent(analytics, {
760
814
  type: "api_ai_error",
@@ -763,6 +817,7 @@ export function createDocsServer(config = {}) {
763
817
  path: requestUrl.pathname,
764
818
  properties: { reason: "disabled" },
765
819
  });
820
+ await emitRunError("disabled", { status: 404 });
766
821
  return new Response(JSON.stringify({
767
822
  error: "AI is not enabled. Set `ai: { enabled: true }` in your docs config to enable it.",
768
823
  }), { status: 404, headers: { "Content-Type": "application/json" } });
@@ -781,6 +836,7 @@ export function createDocsServer(config = {}) {
781
836
  durationMs: Math.max(0, Date.now() - requestStartedAt),
782
837
  },
783
838
  });
839
+ await emitRunError("missing_api_key", { status: 500 });
784
840
  return new Response(JSON.stringify({
785
841
  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
842
  }), { status: 500, headers: { "Content-Type": "application/json" } });
@@ -802,6 +858,7 @@ export function createDocsServer(config = {}) {
802
858
  durationMs: Math.max(0, Date.now() - requestStartedAt),
803
859
  },
804
860
  });
861
+ await emitRunError("invalid_json", { status: 400, locale: ctx.locale });
805
862
  return new Response(JSON.stringify({ error: "Invalid JSON body. Expected { messages: [...] }" }), { status: 400, headers: { "Content-Type": "application/json" } });
806
863
  }
807
864
  const messages = body.messages;
@@ -817,6 +874,7 @@ export function createDocsServer(config = {}) {
817
874
  durationMs: Math.max(0, Date.now() - requestStartedAt),
818
875
  },
819
876
  });
877
+ await emitRunError("missing_messages", { status: 400, locale: ctx.locale });
820
878
  return new Response(JSON.stringify({ error: "messages array is required and must not be empty." }), { status: 400, headers: { "Content-Type": "application/json" } });
821
879
  }
822
880
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
@@ -833,13 +891,69 @@ export function createDocsServer(config = {}) {
833
891
  durationMs: Math.max(0, Date.now() - requestStartedAt),
834
892
  },
835
893
  });
894
+ await emitRunError("missing_user_message", {
895
+ status: 400,
896
+ locale: ctx.locale,
897
+ messageCount: messages.length,
898
+ });
836
899
  return new Response(JSON.stringify({ error: "At least one user message is required." }), {
837
900
  status: 400,
838
901
  headers: { "Content-Type": "application/json" },
839
902
  });
840
903
  }
841
904
  const maxResults = aiConfig.maxResults ?? 5;
905
+ await emitTrace({
906
+ type: "user.input",
907
+ name: "ask-ai",
908
+ parentSpanId: runSpanId,
909
+ startedAt: new Date().toISOString(),
910
+ endedAt: new Date().toISOString(),
911
+ durationMs: 0,
912
+ status: "success",
913
+ locale: ctx.locale,
914
+ inputPreview: {
915
+ messageCount: messages.length,
916
+ questionLength: lastUserMessage.content.length,
917
+ requestedModel: typeof body.model === "string" && body.model.trim().length > 0
918
+ ? body.model.trim()
919
+ : undefined,
920
+ },
921
+ });
922
+ const retrievalStartedAt = Date.now();
923
+ const retrievalStartedAtIso = new Date().toISOString();
924
+ const retrievalSpanId = createDocsAgentTraceId("span");
925
+ await emitTrace({
926
+ type: "retrieval.query",
927
+ name: "docs-index",
928
+ spanId: retrievalSpanId,
929
+ parentSpanId: runSpanId,
930
+ startedAt: retrievalStartedAtIso,
931
+ status: "started",
932
+ locale: ctx.locale,
933
+ inputPreview: {
934
+ queryLength: lastUserMessage.content.length,
935
+ maxResults,
936
+ },
937
+ });
842
938
  const scored = searchByQuery(lastUserMessage.content.toLowerCase(), ctx).slice(0, maxResults);
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");
843
957
  const contextParts = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`);
844
958
  const context = contextParts.join("\n\n---\n\n");
845
959
  const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -853,6 +967,26 @@ export function createDocsServer(config = {}) {
853
967
  systemMessage,
854
968
  ...messages.filter((m) => m.role !== "system"),
855
969
  ];
970
+ await emitTrace({
971
+ type: "prompt.build",
972
+ name: "ask-ai.prompt",
973
+ spanId: promptSpanId,
974
+ parentSpanId: runSpanId,
975
+ startedAt: promptStartedAtIso,
976
+ endedAt: new Date().toISOString(),
977
+ durationMs: Math.max(0, Date.now() - promptStartedAt),
978
+ status: "success",
979
+ locale: ctx.locale,
980
+ inputPreview: {
981
+ messageCount: messages.length,
982
+ retrievedCount: scored.length,
983
+ },
984
+ outputPreview: {
985
+ llmMessageCount: llmMessages.length,
986
+ contextChars: context.length,
987
+ systemMessageChars: systemMessage.content.length,
988
+ },
989
+ });
856
990
  const requestedModel = typeof body.model === "string" && body.model.trim().length > 0
857
991
  ? body.model.trim()
858
992
  : undefined;
@@ -872,16 +1006,96 @@ export function createDocsServer(config = {}) {
872
1006
  model: resolved.model,
873
1007
  },
874
1008
  });
875
- const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
876
- method: "POST",
877
- headers: {
878
- "Content-Type": "application/json",
879
- Authorization: `Bearer ${finalKey}`,
1009
+ const modelStartedAt = Date.now();
1010
+ const modelStartedAtIso = new Date().toISOString();
1011
+ const modelSpanId = createDocsAgentTraceId("span");
1012
+ const providerOrigin = safeUrlOrigin(resolved.baseUrl);
1013
+ await emitTrace({
1014
+ type: "model.call",
1015
+ name: resolved.model,
1016
+ spanId: modelSpanId,
1017
+ parentSpanId: runSpanId,
1018
+ startedAt: modelStartedAtIso,
1019
+ status: "started",
1020
+ locale: ctx.locale,
1021
+ inputPreview: {
1022
+ messageCount: llmMessages.length,
1023
+ stream: true,
1024
+ providerOrigin,
880
1025
  },
881
- body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
1026
+ metadata: { model: resolved.model },
882
1027
  });
1028
+ let llmResponse;
1029
+ try {
1030
+ llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1031
+ method: "POST",
1032
+ headers: {
1033
+ "Content-Type": "application/json",
1034
+ Authorization: `Bearer ${finalKey}`,
1035
+ },
1036
+ body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
1037
+ });
1038
+ }
1039
+ catch (error) {
1040
+ const message = error instanceof Error ? error.message : "Unknown error";
1041
+ await emitTrace({
1042
+ type: "model.error",
1043
+ name: resolved.model,
1044
+ parentSpanId: modelSpanId,
1045
+ startedAt: modelStartedAtIso,
1046
+ endedAt: new Date().toISOString(),
1047
+ durationMs: Math.max(0, Date.now() - modelStartedAt),
1048
+ status: "error",
1049
+ locale: ctx.locale,
1050
+ outputPreview: { message },
1051
+ metadata: { model: resolved.model, providerOrigin },
1052
+ });
1053
+ await emitDocsAnalyticsEvent(analytics, {
1054
+ type: "api_ai_error",
1055
+ source: "server",
1056
+ url: event.request.url,
1057
+ path: requestUrl.pathname,
1058
+ locale: ctx.locale,
1059
+ input: { question: lastUserMessage.content },
1060
+ properties: {
1061
+ reason: "llm_fetch_error",
1062
+ messageCount: messages.length,
1063
+ questionLength: lastUserMessage.content.length,
1064
+ retrievedCount: scored.length,
1065
+ model: resolved.model,
1066
+ durationMs: Math.max(0, Date.now() - requestStartedAt),
1067
+ },
1068
+ });
1069
+ await emitRunError("llm_fetch_error", {
1070
+ status: 502,
1071
+ locale: ctx.locale,
1072
+ messageCount: messages.length,
1073
+ questionLength: lastUserMessage.content.length,
1074
+ retrievedCount: scored.length,
1075
+ model: resolved.model,
1076
+ });
1077
+ return new Response(JSON.stringify({ error: "LLM API request failed." }), {
1078
+ status: 502,
1079
+ headers: { "Content-Type": "application/json" },
1080
+ });
1081
+ }
883
1082
  if (!llmResponse.ok) {
884
1083
  const errText = await llmResponse.text().catch(() => "Unknown error");
1084
+ await emitTrace({
1085
+ type: "model.error",
1086
+ name: resolved.model,
1087
+ parentSpanId: modelSpanId,
1088
+ startedAt: modelStartedAtIso,
1089
+ endedAt: new Date().toISOString(),
1090
+ durationMs: Math.max(0, Date.now() - modelStartedAt),
1091
+ status: "error",
1092
+ locale: ctx.locale,
1093
+ outputPreview: {
1094
+ status: llmResponse.status,
1095
+ errorChars: errText.length,
1096
+ },
1097
+ metadata: { model: resolved.model, providerOrigin },
1098
+ });
885
1099
  await emitDocsAnalyticsEvent(analytics, {
886
1100
  type: "api_ai_error",
887
1101
  source: "server",
@@ -899,6 +1113,15 @@ export function createDocsServer(config = {}) {
899
1113
  durationMs: Math.max(0, Date.now() - requestStartedAt),
900
1114
  },
901
1115
  });
1116
+ await emitRunError("llm_error", {
1117
+ status: 502,
1118
+ modelStatus: llmResponse.status,
1119
+ locale: ctx.locale,
1120
+ messageCount: messages.length,
1121
+ questionLength: lastUserMessage.content.length,
1122
+ retrievedCount: scored.length,
1123
+ model: resolved.model,
1124
+ });
902
1125
  return new Response(JSON.stringify({ error: `LLM API error (${llmResponse.status}): ${errText}` }), { status: 502, headers: { "Content-Type": "application/json" } });
903
1126
  }
904
1127
  await emitDocsAnalyticsEvent(analytics, {
@@ -916,6 +1139,67 @@ export function createDocsServer(config = {}) {
916
1139
  durationMs: Math.max(0, Date.now() - requestStartedAt),
917
1140
  },
918
1141
  });
1142
+ const responseEndedAt = new Date().toISOString();
1143
+ const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
1144
+ await emitTrace({
1145
+ type: "model.response",
1146
+ name: resolved.model,
1147
+ parentSpanId: modelSpanId,
1148
+ startedAt: modelStartedAtIso,
1149
+ endedAt: responseEndedAt,
1150
+ durationMs: modelDurationMs,
1151
+ status: "success",
1152
+ locale: ctx.locale,
1153
+ outputPreview: {
1154
+ status: llmResponse.status,
1155
+ stream: true,
1156
+ contentType: llmResponse.headers.get("content-type") ?? undefined,
1157
+ },
1158
+ metadata: { model: resolved.model, providerOrigin },
1159
+ });
1160
+ await emitTrace({
1161
+ type: "model.stream",
1162
+ name: resolved.model,
1163
+ parentSpanId: modelSpanId,
1164
+ startedAt: modelStartedAtIso,
1165
+ endedAt: responseEndedAt,
1166
+ durationMs: modelDurationMs,
1167
+ status: "success",
1168
+ locale: ctx.locale,
1169
+ outputPreview: { stream: true },
1170
+ metadata: { model: resolved.model },
1171
+ });
1172
+ const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
1173
+ await emitTrace({
1174
+ type: "agent.final",
1175
+ name: "ask-ai",
1176
+ parentSpanId: runSpanId,
1177
+ startedAt: trace.startedAt,
1178
+ endedAt: new Date().toISOString(),
1179
+ durationMs: runDurationMs,
1180
+ status: "success",
1181
+ locale: ctx.locale,
1182
+ outputPreview: {
1183
+ stream: true,
1184
+ retrievedCount: scored.length,
1185
+ },
1186
+ metadata: { model: resolved.model },
1187
+ });
1188
+ await emitTrace({
1189
+ type: "run.end",
1190
+ name: "ask-ai",
1191
+ spanId: runSpanId,
1192
+ startedAt: trace.startedAt,
1193
+ endedAt: new Date().toISOString(),
1194
+ durationMs: runDurationMs,
1195
+ status: "success",
1196
+ locale: ctx.locale,
1197
+ outputPreview: {
1198
+ stream: true,
1199
+ retrievedCount: scored.length,
1200
+ },
1201
+ metadata: { model: resolved.model },
1202
+ });
919
1203
  return new Response(llmResponse.body, {
920
1204
  headers: {
921
1205
  "Content-Type": "text/event-stream",
@@ -947,6 +1231,7 @@ export function createDocsServer(config = {}) {
947
1231
  },
948
1232
  mcp: config.mcp,
949
1233
  analytics,
1234
+ observability,
950
1235
  defaultName: mcpSiteTitle,
951
1236
  });
952
1237
  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.70",
3
+ "version": "0.1.72",
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.70"
59
+ "@farming-labs/docs": "0.1.72"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@farming-labs/docs": "*"