@umituz/web-ai 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -90,6 +90,12 @@ The pieces a backend-proxy chat stack needs, all persistence-free:
90
90
  - `buildUsageDailySeries` / `usageWindowTotals` / `usageTrendOf` /
91
91
  `errorRatePct` — turn sparse SQL day aggregates into zero-filled usage
92
92
  series and dashboard math.
93
+ - `deriveConversationTitle(content, { fallback, maxChars })` — first-line
94
+ title derivation for new conversations (Markdown prefix strip, collapse,
95
+ truncate).
96
+ - `planStuckRecovery(task, now)` / `STUCK_TASK_THRESHOLD_MS` — recovery
97
+ decision for queue rows found stale in `running`; sits next to
98
+ `canStartTask` / `planTaskFailure`.
93
99
 
94
100
  ## Client-side chat over your own backend
95
101
 
@@ -322,6 +322,31 @@ function aiErrorStatus(code) {
322
322
  return AI_ERROR_STATUS[code];
323
323
  }
324
324
 
325
+ // src/core/errors/ai-failure.ts
326
+ var TYPE_TO_CODE = {
327
+ ["INVALID_API_KEY" /* INVALID_API_KEY */]: "invalid_key",
328
+ ["MISSING_CONFIG" /* MISSING_CONFIG */]: "missing_key",
329
+ ["NETWORK_ERROR" /* NETWORK_ERROR */]: "network",
330
+ ["ABORT_ERROR" /* ABORT_ERROR */]: "timeout",
331
+ ["RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */]: "rate_limited",
332
+ ["SERVER_ERROR" /* SERVER_ERROR */]: "upstream_error",
333
+ ["CLIENT_ERROR" /* CLIENT_ERROR */]: "invalid_request",
334
+ ["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: "upstream_error"
335
+ };
336
+ function aiFailureOf(error) {
337
+ if (error instanceof AiError) {
338
+ return { code: error.code, status: AI_ERROR_STATUS[error.code], message: error.userMessage };
339
+ }
340
+ if (error instanceof AIError) {
341
+ const code = TYPE_TO_CODE[error.type];
342
+ return { code, status: AI_ERROR_STATUS[code], message: CODE_MESSAGES[code] };
343
+ }
344
+ if (error instanceof Error && error.name === "AbortError") {
345
+ return { code: "timeout", status: AI_ERROR_STATUS.timeout, message: CODE_MESSAGES.timeout };
346
+ }
347
+ return { code: "upstream_error", status: AI_ERROR_STATUS.upstream_error, message: CODE_MESSAGES.upstream_error };
348
+ }
349
+
325
350
  // src/core/executor/retry-executor.strategy.ts
326
351
  var RetryExecutor = class {
327
352
  constructor(retryManager2, options) {
@@ -925,6 +950,34 @@ function failoverCandidate(primary, fallback) {
925
950
  return fallback;
926
951
  }
927
952
 
953
+ // src/core/utils/title.ts
954
+ function deriveConversationTitle(content, options = {}) {
955
+ const fallback = options.fallback ?? "";
956
+ const maxChars = options.maxChars ?? 60;
957
+ const firstLine = content.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
958
+ const cleaned = (firstLine ?? fallback).replace(/^#{1,6}\s+/, "").replace(/^[>*-]\s+/, "").replace(/\s+/g, " ");
959
+ if (cleaned.length === 0) return fallback;
960
+ return cleaned.length > maxChars ? `${cleaned.slice(0, maxChars).trimEnd()}\u2026` : cleaned;
961
+ }
962
+
963
+ // src/core/utils/allowance-window.ts
964
+ var DEFAULT_ALLOWANCE_WINDOW_MS = 36e5;
965
+ function createAllowanceWindow(options) {
966
+ const windowMs = options.windowMs ?? DEFAULT_ALLOWANCE_WINDOW_MS;
967
+ const buckets = /* @__PURE__ */ new Map();
968
+ return {
969
+ allow(key, now = Date.now()) {
970
+ const bucket = buckets.get(key);
971
+ if (bucket === void 0 || now - bucket.windowStart >= windowMs) {
972
+ buckets.set(key, { windowStart: now, count: 1 });
973
+ return true;
974
+ }
975
+ bucket.count += 1;
976
+ return bucket.count <= options.limit;
977
+ }
978
+ };
979
+ }
980
+
928
981
  // src/core/constants/protocol.constants.ts
929
982
  var SSE_PREFIX = "data: ";
930
983
  var SSE_DONE_MARKER = "[DONE]";
@@ -1304,6 +1357,11 @@ function planTaskFailure(task, errorDetail, backoffBaseMs, now = /* @__PURE__ */
1304
1357
  }
1305
1358
  };
1306
1359
  }
1360
+ var STUCK_TASK_THRESHOLD_MS = 15 * 60 * 1e3;
1361
+ function planStuckRecovery(task, now = /* @__PURE__ */ new Date(), thresholdMs = STUCK_TASK_THRESHOLD_MS) {
1362
+ if (now.getTime() - task.updatedAt.getTime() < thresholdMs) return null;
1363
+ return task.attempts >= task.maxAttempts ? { action: "fail" } : { action: "requeue" };
1364
+ }
1307
1365
 
1308
1366
  // src/core/sse/sse-response.service.ts
1309
1367
  var SSE_BRIDGE_EVENTS = {
@@ -1903,21 +1961,52 @@ async function readSseResponse(response, callbacks) {
1903
1961
  callbacks.onError?.("request_failed", "The server returned an empty stream.");
1904
1962
  return;
1905
1963
  }
1906
- const reader = response.body.getReader();
1964
+ for await (const event of iterateSseJson(response)) {
1965
+ if (event) dispatchSseEvent(event, callbacks);
1966
+ }
1967
+ }
1968
+ function dataPayloadOf(frame) {
1969
+ let data = null;
1970
+ for (const line of frame.split("\n")) {
1971
+ if (line.startsWith("data:")) {
1972
+ const payload = line.slice(5).trim();
1973
+ data = data === null ? payload : `${data}
1974
+ ${payload}`;
1975
+ }
1976
+ }
1977
+ return data !== null && data.length > 0 ? data : null;
1978
+ }
1979
+ async function* iterateSseData(source) {
1980
+ const body = source instanceof Response ? source.body : source;
1981
+ if (!body) {
1982
+ throw new Error("iterateSseData: the response has no body to read");
1983
+ }
1984
+ const reader = body.getReader();
1907
1985
  const decoder = new TextDecoder();
1908
1986
  let buffer = "";
1909
- while (true) {
1910
- const { done, value } = await reader.read();
1911
- if (done) break;
1912
- buffer += decoder.decode(value, { stream: true });
1913
- let separator = buffer.indexOf("\n\n");
1914
- while (separator !== -1) {
1915
- const rawEvent = buffer.slice(0, separator);
1916
- buffer = buffer.slice(separator + 2);
1917
- separator = buffer.indexOf("\n\n");
1918
- const event = parseSseEvent(rawEvent);
1919
- if (!event) continue;
1920
- dispatchSseEvent(event, callbacks);
1987
+ try {
1988
+ while (true) {
1989
+ const { done, value } = await reader.read();
1990
+ if (done) break;
1991
+ buffer += decoder.decode(value, { stream: true });
1992
+ let boundary = buffer.indexOf("\n\n");
1993
+ while (boundary !== -1) {
1994
+ const frame = buffer.slice(0, boundary);
1995
+ buffer = buffer.slice(boundary + 2);
1996
+ boundary = buffer.indexOf("\n\n");
1997
+ const data = dataPayloadOf(frame);
1998
+ if (data) yield data;
1999
+ }
2000
+ }
2001
+ } finally {
2002
+ await reader.cancel().catch(() => void 0);
2003
+ }
2004
+ }
2005
+ async function* iterateSseJson(source) {
2006
+ for await (const data of iterateSseData(source)) {
2007
+ try {
2008
+ yield JSON.parse(data);
2009
+ } catch {
1921
2010
  }
1922
2011
  }
1923
2012
  }
@@ -2062,6 +2151,7 @@ exports.CODE_MESSAGES = CODE_MESSAGES;
2062
2151
  exports.CacheManager = CacheManager;
2063
2152
  exports.CompositeExecutor = CompositeExecutor;
2064
2153
  exports.DEFAULT_AI_IDENTITY = DEFAULT_AI_IDENTITY;
2154
+ exports.DEFAULT_ALLOWANCE_WINDOW_MS = DEFAULT_ALLOWANCE_WINDOW_MS;
2065
2155
  exports.DEFAULT_API_KEY_RULE = DEFAULT_API_KEY_RULE;
2066
2156
  exports.DEFAULT_EMBED_BATCH_SIZE = DEFAULT_EMBED_BATCH_SIZE;
2067
2157
  exports.DEFAULT_ERROR_CLASSIFICATION_RULE = DEFAULT_ERROR_CLASSIFICATION_RULE;
@@ -2100,6 +2190,7 @@ exports.SSE_BRIDGE_EVENTS = SSE_BRIDGE_EVENTS;
2100
2190
  exports.SSE_DONE_MARKER = SSE_DONE_MARKER;
2101
2191
  exports.SSE_PREFIX = SSE_PREFIX;
2102
2192
  exports.STATUS_TO_CODE = STATUS_TO_CODE;
2193
+ exports.STUCK_TASK_THRESHOLD_MS = STUCK_TASK_THRESHOLD_MS;
2103
2194
  exports.SYSTEM_PROMPT_VERSION = SYSTEM_PROMPT_VERSION;
2104
2195
  exports.StreamingResponseParser = StreamingResponseParser;
2105
2196
  exports.StructuredPromptBuilder = StructuredPromptBuilder;
@@ -2113,6 +2204,7 @@ exports.USER_FRIENDLY_ERROR_MESSAGES = USER_FRIENDLY_ERROR_MESSAGES;
2113
2204
  exports.accumulateReport = accumulateReport;
2114
2205
  exports.aiErrorMessage = aiErrorMessage;
2115
2206
  exports.aiErrorStatus = aiErrorStatus;
2207
+ exports.aiFailureOf = aiFailureOf;
2116
2208
  exports.asObjectArray = asObjectArray;
2117
2209
  exports.asRecord = asRecord;
2118
2210
  exports.asStringArray = asStringArray;
@@ -2138,11 +2230,13 @@ exports.classifyHttpStatus = classifyHttpStatus;
2138
2230
  exports.computeCostMicros = computeCostMicros;
2139
2231
  exports.cosineSimilarity = cosineSimilarity;
2140
2232
  exports.countChanges = countChanges;
2233
+ exports.createAllowanceWindow = createAllowanceWindow;
2141
2234
  exports.createRemoteChatClient = createRemoteChatClient;
2142
2235
  exports.createSseResponse = createSseResponse;
2143
2236
  exports.debounce = debounce;
2144
2237
  exports.debounceManager = debounceManager;
2145
2238
  exports.defaultSseDeltaExtractor = defaultSseDeltaExtractor;
2239
+ exports.deriveConversationTitle = deriveConversationTitle;
2146
2240
  exports.deriveSnippetTitle = deriveSnippetTitle;
2147
2241
  exports.describeAIErrorType = describeAIErrorType;
2148
2242
  exports.diffLines = diffLines;
@@ -2175,6 +2269,8 @@ exports.isTextTier = isTextTier;
2175
2269
  exports.isTransientError = isTransientError;
2176
2270
  exports.isValidApiKey = isValidApiKey;
2177
2271
  exports.isWithinWindow = isWithinWindow;
2272
+ exports.iterateSseData = iterateSseData;
2273
+ exports.iterateSseJson = iterateSseJson;
2178
2274
  exports.magnitude = magnitude;
2179
2275
  exports.mergeTierOverrides = mergeTierOverrides;
2180
2276
  exports.nextAttemptDelayMs = nextAttemptDelayMs;
@@ -2183,6 +2279,7 @@ exports.normalizeSources = normalizeSources;
2183
2279
  exports.normalizeSseChunk = normalizeSseChunk;
2184
2280
  exports.oneOfCatalog = oneOfCatalog;
2185
2281
  exports.parseSseEvent = parseSseEvent;
2282
+ exports.planStuckRecovery = planStuckRecovery;
2186
2283
  exports.planTaskFailure = planTaskFailure;
2187
2284
  exports.readSseResponse = readSseResponse;
2188
2285
  exports.repairCommonJsonDefects = repairCommonJsonDefects;