@123toto/ai-app-assistant-server 0.2.0 → 0.2.2

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
@@ -44,6 +44,7 @@ import { createManagedAiAppAssistantServer } from "@123toto/ai-app-assistant-ser
44
44
 
45
45
  const aiAppAssistant = createManagedAiAppAssistantServer({
46
46
  configuration: {
47
+ enabled: process.env.AI_APP_ASSISTANT_LLM_ENABLED === "true",
47
48
  storage: { type: "redis", client: redis },
48
49
  encryptionKey: process.env.AI_APP_ASSISTANT_SECRET_ENCRYPTION_KEY!,
49
50
  defaultConfiguration: {
@@ -63,6 +64,26 @@ const aiAppAssistant = createManagedAiAppAssistantServer({
63
64
  await aiAppAssistant.initialize();
64
65
  ```
65
66
 
67
+ For deployments exposing one provider at a time, the library maps generic
68
+ runtime values without requiring provider-specific environment variables:
69
+
70
+ ```ts
71
+ const deployment = createAiAppAssistantDeploymentDefaults({
72
+ enabled: env.AI_APP_ASSISTANT_LLM_ENABLED,
73
+ provider: env.AI_APP_ASSISTANT_PROVIDER,
74
+ model: env.AI_APP_ASSISTANT_LLM_MODEL,
75
+ apiKey: env.AI_APP_ASSISTANT_API_KEY,
76
+ baseURL: env.AI_APP_ASSISTANT_LOCAL_LLM_BASE_URL
77
+ });
78
+ ```
79
+
80
+ `baseURL` is required for the local `ollama` provider. API keys and encryption
81
+ keys must come from a runtime secret store; they must never be committed or
82
+ embedded in an application image.
83
+
84
+ `enabled: false` disables every provider operation while keeping the host
85
+ application and the read-only administration endpoints available.
86
+
66
87
  Documents may also be loaded after application bootstrap with `aiAppAssistant.setDocuments(documents)`.
67
88
 
68
89
  ## Framework connectors
@@ -1,4 +1,4 @@
1
- import { M as ManagedAiAppAssistantServer } from './managed-server-CthTKEQ6.cjs';
1
+ import { M as ManagedAiAppAssistantServer } from './managed-server-wXfu_9rp.cjs';
2
2
  import './ai-sdk-qU3KgNu-.cjs';
3
3
  import 'ai';
4
4
  import '@123toto/ai-app-assistant-contracts';
package/dist/express.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { M as ManagedAiAppAssistantServer } from './managed-server-BGQ-1jkx.js';
1
+ import { M as ManagedAiAppAssistantServer } from './managed-server-CusQWWBB.js';
2
2
  import './ai-sdk-qU3KgNu-.js';
3
3
  import 'ai';
4
4
  import '@123toto/ai-app-assistant-contracts';
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AI_APP_ASSISTANT_PROVIDERS: () => AI_APP_ASSISTANT_PROVIDERS,
23
24
  AiAppAssistantConfigurationConflictError: () => AiAppAssistantConfigurationConflictError,
24
25
  AiAppAssistantConfigurationManager: () => AiAppAssistantConfigurationManager,
25
26
  AiAppAssistantManagementError: () => AiAppAssistantManagementError,
@@ -1464,6 +1465,7 @@ function quotaResult(count, resetAt, maxRequests, now) {
1464
1465
  }
1465
1466
 
1466
1467
  // src/provider-catalog.ts
1468
+ var AI_APP_ASSISTANT_PROVIDERS = ["anthropic", "google", "mistral", "ollama", "openai"];
1467
1469
  var PROVIDERS = Object.freeze([
1468
1470
  { id: "anthropic", label: "Anthropic", requiresApiKey: true, supportsModelDiscovery: true },
1469
1471
  { id: "google", label: "Google Gemini", requiresApiKey: true, supportsModelDiscovery: true },
@@ -1609,12 +1611,17 @@ var AiAppAssistantConfigurationManager = class {
1609
1611
  this.#repository = options.repository;
1610
1612
  this.#quotaStore = options.quotaStore ?? createMemoryAiAppAssistantQuotaStore();
1611
1613
  }
1614
+ /** Returns the current state of the host-controlled global kill switch. */
1615
+ isEnabled() {
1616
+ return typeof this.#options.enabled === "function" ? this.#options.enabled() : this.#options.enabled ?? true;
1617
+ }
1612
1618
  /** Returns the built-in provider catalogue; no credentials are exposed. */
1613
1619
  listProviders() {
1614
1620
  return listAiProviders();
1615
1621
  }
1616
1622
  /** Discovers models with an explicit key or the currently configured secret. */
1617
1623
  async listModels(input) {
1624
+ this.assertEnabled();
1618
1625
  const apiKey = await this.resolveApiKey(input.provider, input.apiKey);
1619
1626
  if (this.#options.listModels) {
1620
1627
  return this.#options.listModels({ ...input, ...apiKey ? { apiKey } : {} });
@@ -1640,7 +1647,10 @@ var AiAppAssistantConfigurationManager = class {
1640
1647
  let connected = event.connectionValidated;
1641
1648
  if (event.reloadRequired) {
1642
1649
  this.#recentConnectionValidation = void 0;
1643
- if (connected) {
1650
+ if (!this.isEnabled()) {
1651
+ connected = false;
1652
+ this.#runtimeConnection = { status: "disabled", checkedAt: this.now() };
1653
+ } else if (connected) {
1644
1654
  const configuration = await this.getRuntimeConfiguration();
1645
1655
  this.#runtimeConnection = configuration ? {
1646
1656
  status: "connected",
@@ -1670,6 +1680,7 @@ var AiAppAssistantConfigurationManager = class {
1670
1680
  }
1671
1681
  /** Tests credentials and briefly caches a successful result for the next save. */
1672
1682
  async testConnection(input) {
1683
+ this.assertEnabled();
1673
1684
  const apiKey = await this.resolveApiKey(input.provider, input.apiKey);
1674
1685
  const connection = this.#options.testConnection ? await this.#options.testConnection({ ...input, ...apiKey ? { apiKey } : {} }) : await testAiSdkConnection({
1675
1686
  model: `${input.provider}:${input.model}`,
@@ -1698,6 +1709,10 @@ var AiAppAssistantConfigurationManager = class {
1698
1709
  /** Checks the effective stored/deployment connection used by live questions. */
1699
1710
  async validateRuntimeConnection() {
1700
1711
  this.#lastReconnectAttempt = Date.now();
1712
+ if (!this.isEnabled()) {
1713
+ this.#runtimeConnection = { status: "disabled", checkedAt: this.now() };
1714
+ return false;
1715
+ }
1701
1716
  const configuration = await this.getRuntimeConfiguration();
1702
1717
  if (!configuration || configuration.provider !== "ollama" && !configuration.apiKey) {
1703
1718
  this.#runtimeConnection = {
@@ -1740,6 +1755,7 @@ var AiAppAssistantConfigurationManager = class {
1740
1755
  const initialActive = this.effectiveConfiguration(initial);
1741
1756
  const initialApiKey = input.apiKey ?? (initialActive?.provider === input.provider ? initialActive.apiKey : void 0) ?? this.resolveDefaultApiKey(input.provider);
1742
1757
  const initialConnectionChanged = connectionChanged(initialActive, input);
1758
+ if (!this.isEnabled() && initialConnectionChanged) throw disabled();
1743
1759
  let connection = initialConnectionChanged ? await this.validateConnectionForSave(input, initialApiKey) : this.lastKnownConnection(input.provider, input.model);
1744
1760
  if (initialConnectionChanged && !connection.success) {
1745
1761
  return { saved: false, connection, reloadRequired: false };
@@ -1968,6 +1984,7 @@ var AiAppAssistantConfigurationManager = class {
1968
1984
  }
1969
1985
  /** Combines configuration, provider health and application access rules. */
1970
1986
  async canUse(identity) {
1987
+ if (!this.isEnabled()) return false;
1971
1988
  const configuration = await this.getRuntimeConfiguration();
1972
1989
  if (!configuration || configuration.provider !== "ollama" && !configuration.apiKey) return false;
1973
1990
  if (!await this.ensureRuntimeConnection()) return false;
@@ -1986,6 +2003,7 @@ var AiAppAssistantConfigurationManager = class {
1986
2003
  }
1987
2004
  /** Enforces access and quota immediately before any model call. */
1988
2005
  async assertCanAsk(identity) {
2006
+ this.assertEnabled();
1989
2007
  if (!await this.canUse(identity)) {
1990
2008
  throw forbidden("AI assistant access is not enabled for this user");
1991
2009
  }
@@ -2001,6 +2019,7 @@ var AiAppAssistantConfigurationManager = class {
2001
2019
  }
2002
2020
  /** Retries a failed provider lazily, with a shared backoff across requests. */
2003
2021
  async ensureRuntimeConnection() {
2022
+ if (!this.isEnabled()) return false;
2004
2023
  if (this.#runtimeConnection.status === "connected") return true;
2005
2024
  const intervalMs = Math.max(1e3, this.#options.reconnectIntervalMs ?? 3e4);
2006
2025
  if (Date.now() - this.#lastReconnectAttempt < intervalMs) return false;
@@ -2108,6 +2127,9 @@ var AiAppAssistantConfigurationManager = class {
2108
2127
  now() {
2109
2128
  return (this.#options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
2110
2129
  }
2130
+ assertEnabled() {
2131
+ if (!this.isEnabled()) throw disabled();
2132
+ }
2111
2133
  async publishAndEmit(event) {
2112
2134
  await this.#options.synchronizer?.publish(event);
2113
2135
  await this.emit(event);
@@ -2231,6 +2253,13 @@ function configurationChanges(previous, input, allowModelChangesByOthers) {
2231
2253
  function forbidden(message) {
2232
2254
  return new AiAppAssistantManagementError(403, "forbidden", message);
2233
2255
  }
2256
+ function disabled() {
2257
+ return new AiAppAssistantManagementError(
2258
+ 503,
2259
+ "assistant_disabled",
2260
+ "AI assistant is disabled"
2261
+ );
2262
+ }
2234
2263
 
2235
2264
  // src/telemetry.ts
2236
2265
  function createMemoryAiAppAssistantTelemetryStore(options) {
@@ -2400,6 +2429,11 @@ function createManagedAiAppAssistantRuntime(options) {
2400
2429
  return reloadQueue;
2401
2430
  });
2402
2431
  const rebuild = async (event) => {
2432
+ if (!options.configuration.isEnabled()) {
2433
+ assistant = void 0;
2434
+ activeGenerator = void 0;
2435
+ return;
2436
+ }
2403
2437
  const connected = event.connectionValidated || await options.configuration.validateRuntimeConnection();
2404
2438
  const configuration = await options.configuration.getRuntimeConfiguration();
2405
2439
  if (!connected || !configuration) {
@@ -2786,21 +2820,38 @@ function createAiAppAssistantDeploymentDefaults(options) {
2786
2820
  const apiKeys = Object.fromEntries(
2787
2821
  Object.entries(options.apiKeys ?? {}).map(([provider2, value]) => [provider2, value?.trim() || void 0])
2788
2822
  );
2789
- const resolveApiKey2 = (provider2) => apiKeys[provider2];
2823
+ const selectedProvider = options.provider;
2824
+ const selectedApiKey = options.apiKey?.trim() || void 0;
2825
+ const resolveApiKey2 = (provider2) => provider2 === selectedProvider ? selectedApiKey ?? apiKeys[provider2] : apiKeys[provider2];
2790
2826
  if (!options.enabled || !options.model?.trim()) return { resolveApiKey: resolveApiKey2 };
2791
- const match = options.model.trim().match(/^([^:/]+)[:/](.+)$/);
2792
- if (!match) throw new TypeError(`Invalid AI App Assistant model identifier: ${options.model}`);
2793
- const provider = match[1] === "gemini" ? "google" : match[1];
2827
+ const rawModel = options.model.trim();
2828
+ const legacyMatch = selectedProvider ? void 0 : rawModel.match(/^([^:/]+)[:/](.+)$/);
2829
+ const explicitMatch = selectedProvider ? rawModel.match(/^([^:]+):(.+)$/) : void 0;
2830
+ if (!selectedProvider && !legacyMatch) {
2831
+ throw new TypeError(`Invalid AI App Assistant model identifier: ${options.model}`);
2832
+ }
2833
+ const parsedProvider = legacyMatch?.[1] ?? explicitMatch?.[1];
2834
+ const modelProvider = parsedProvider === "gemini" ? "google" : parsedProvider ?? selectedProvider;
2835
+ const provider = modelProvider;
2794
2836
  if (!listAiProviders().some((candidate) => candidate.id === provider)) {
2795
- throw new TypeError(`Unsupported AI App Assistant provider: ${match[1]}`);
2837
+ throw new TypeError(`Unsupported AI App Assistant provider: ${modelProvider}`);
2796
2838
  }
2839
+ if (selectedProvider && explicitMatch && provider !== selectedProvider) {
2840
+ throw new TypeError(
2841
+ `AI App Assistant provider '${selectedProvider}' does not match model identifier '${options.model}'`
2842
+ );
2843
+ }
2844
+ const model = legacyMatch?.[2]?.trim() ?? explicitMatch?.[2]?.trim() ?? rawModel;
2797
2845
  const apiKey = resolveApiKey2(provider);
2798
- const baseURL = options.baseURLs?.[provider]?.trim() || void 0;
2846
+ const baseURL = (provider === selectedProvider ? options.baseURL?.trim() : void 0) || options.baseURLs?.[provider]?.trim() || void 0;
2847
+ if (provider === "ollama" && !baseURL) {
2848
+ throw new TypeError("AI App Assistant local provider 'ollama' requires a base URL");
2849
+ }
2799
2850
  return {
2800
2851
  resolveApiKey: resolveApiKey2,
2801
2852
  configuration: {
2802
2853
  provider,
2803
- model: match[2].trim(),
2854
+ model,
2804
2855
  connectionSource: "environment",
2805
2856
  ...apiKey ? { apiKey } : {},
2806
2857
  ...baseURL ? { baseURL } : {},
@@ -3010,6 +3061,7 @@ function clampInteger3(value, minimum, maximum) {
3010
3061
  var import_ai_app_assistant_contracts6 = require("@123toto/ai-app-assistant-contracts");
3011
3062
  // Annotate the CommonJS export names for ESM import in node:
3012
3063
  0 && (module.exports = {
3064
+ AI_APP_ASSISTANT_PROVIDERS,
3013
3065
  AiAppAssistantConfigurationConflictError,
3014
3066
  AiAppAssistantConfigurationManager,
3015
3067
  AiAppAssistantManagementError,