@equinor/fusion-framework-cli-plugin-ai-base 2.0.1 → 3.0.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.
@@ -1,42 +1,22 @@
1
1
  /**
2
- * Hand-authored TypeScript interface for AI CLI command options.
2
+ * Base options for all Fusion AI CLI commands.
3
3
  *
4
- * @remarks
5
- * Use {@link AiOptions} when you need a lightweight type without pulling in Zod.
6
- * For runtime validation prefer {@link AiOptionsSchema} from the schema module.
7
- *
8
- * @packageDocumentation
9
- */
10
-
11
- /**
12
- * Configuration options for AI-related CLI commands.
13
- *
14
- * This interface defines all available options for configuring Azure OpenAI services
15
- * and Azure Cognitive Search integration. Required fields must be provided either
16
- * via command-line arguments or environment variables. Optional fields enable
17
- * specific features (chat, embeddings, vector search) when provided.
18
- *
19
- * @remarks
20
- * - All required fields (apiKey, apiVersion, instance) must be provided for any AI operation
21
- * - Chat operations require `openaiChatDeployment`
22
- * - Embedding operations require `openaiEmbeddingDeployment`
23
- * - Vector search requires all three Azure Search fields plus `openaiEmbeddingDeployment`
4
+ * The service URL and token are resolved automatically from Fusion
5
+ * service discovery using the provided environment and authentication options.
24
6
  */
25
7
  export interface AiOptions {
26
- /** Azure OpenAI API key for authentication with Azure OpenAI services */
27
- openaiApiKey: string;
28
- /** Azure OpenAI API version (e.g., '2024-02-15-preview') */
29
- openaiApiVersion: string;
30
- /** Azure OpenAI instance name (the resource name in Azure) */
31
- openaiInstance: string;
32
- /** Azure OpenAI chat model deployment name. Required for chat operations */
33
- openaiChatDeployment?: string;
34
- /** Azure OpenAI embedding model deployment name. Required for embedding and vector search operations */
35
- openaiEmbeddingDeployment?: string;
36
- /** Azure Cognitive Search endpoint URL. Required for vector search operations */
37
- azureSearchEndpoint?: string;
38
- /** Azure Cognitive Search API key. Required for vector search operations */
39
- azureSearchApiKey?: string;
40
- /** Azure Cognitive Search index name. Required for vector search operations */
41
- azureSearchIndexName?: string;
8
+ /** Fusion environment used for service discovery (e.g. `ci`, `fprd`). */
9
+ env?: string;
10
+ /** Bearer token passed directly to the auth module (overrides clientId/tenantId). */
11
+ token?: string;
12
+ /** Azure AD tenant ID for MSAL silent authentication. */
13
+ tenantId?: string;
14
+ /** Azure AD client ID for MSAL silent authentication. */
15
+ clientId?: string;
16
+ /** Azure OpenAI chat model deployment name. Required for chat operations. */
17
+ chatModel?: string;
18
+ /** Azure OpenAI embedding model deployment name. Required for embedding and index operations. */
19
+ embedModel?: string;
20
+ /** Azure AI Search index name. Required for vector search / indexing operations. */
21
+ indexName?: string;
42
22
  }
@@ -1,159 +1,65 @@
1
1
  import { type Command, InvalidOptionArgumentError } from 'commander';
2
2
  import {
3
- apiInstanceOption,
4
- apiKeyOption,
5
- apiVersionOption,
6
- azureSearchApiKeyOption,
7
- azureSearchEndpointOption,
8
- azureSearchIndexNameOption,
9
- chatDeploymentOption,
10
- embeddingDeploymentOption,
3
+ chatModelOption,
4
+ clientIdOption,
5
+ embedModelOption,
6
+ envOption,
7
+ indexNameOption,
8
+ tenantIdOption,
9
+ tokenOption,
11
10
  } from './options.js';
12
11
 
13
12
  /**
14
- * Enhances a Commander command with AI-related options and validation.
13
+ * Enhances a Commander command with Fusion AI options and validation.
15
14
  *
16
- * This function adds Azure OpenAI and Azure Cognitive Search options to the provided
17
- * command, along with pre-action validation hooks to ensure required options are provided.
18
- * The function allows selective inclusion of chat, embedding, and search capabilities
19
- * based on the command's requirements.
15
+ * Core auth options (`--env`, `--token`, `--tenant-id`, `--client-id`) are always
16
+ * added. When no explicit `--token` is provided, the framework will authenticate
17
+ * via MSAL and resolve the AI service endpoint from Fusion service discovery.
20
18
  *
21
- * Options added:
22
- * - Core: `openaiApiKey`, `openaiApiVersion`, `openaiInstance` (always included)
23
- * - Chat: `openaiChatDeployment` (if includeChat is true)
24
- * - Embedding: `openaiEmbeddingDeployment` (if includeEmbedding is true)
25
- * - Search: `azureSearchEndpoint`, `azureSearchApiKey`, `azureSearchIndexName` (if includeSearch is true)
19
+ * Pass flags to include optional chat, embed, or index options and make
20
+ * them required at runtime via the `preAction` validation hook.
26
21
  *
27
- * @param command - The Commander command instance to enhance with AI options
28
- * @param args - Optional configuration object for selective feature inclusion
29
- * @param args.includeEmbedding - Whether to include and require embedding deployment option (default: false)
30
- * @param args.includeChat - Whether to include and require chat deployment option (default: false)
31
- * @param args.includeSearch - Whether to include and require Azure Search options (default: false)
32
- * @returns The enhanced command with AI options and validation hooks attached
33
- * @throws {InvalidOptionArgumentError} During command execution if required options are missing or invalid
34
- *
35
- * @example
36
- * ```ts
37
- * const chatCommand = createCommand('chat')
38
- * .description('Start a chat session');
39
- *
40
- * withOptions(chatCommand, { includeChat: true });
41
- * ```
22
+ * @param command - The Commander command to decorate with options.
23
+ * @param args - Feature flags controlling which optional options to add.
24
+ * @param args.includeChat - Add `--chat-model` and validate it at runtime.
25
+ * @param args.includeEmbedding - Add `--embed-model` and validate it at runtime.
26
+ * @param args.includeSearch - Add `--index-name` and validate it at runtime.
27
+ * @returns The decorated command.
42
28
  */
43
29
  export const withOptions = (
44
30
  command: Command,
45
31
  args?: Partial<{
46
- includeEmbedding: boolean;
47
32
  includeChat: boolean;
33
+ includeEmbedding: boolean;
48
34
  includeSearch: boolean;
49
35
  }>,
50
36
  ): Command => {
51
- // Core authentication options
52
- command.addOption(apiKeyOption);
53
- command.addOption(apiVersionOption);
54
- command.addOption(apiInstanceOption);
55
-
56
- // Deployment options
57
- if (args?.includeChat === true) {
58
- command.addOption(chatDeploymentOption);
59
- }
37
+ command.addOption(envOption);
38
+ command.addOption(tokenOption);
39
+ command.addOption(tenantIdOption);
40
+ command.addOption(clientIdOption);
60
41
 
61
- if (args?.includeEmbedding === true) {
62
- command.addOption(embeddingDeploymentOption);
63
- }
42
+ if (args?.includeChat) command.addOption(chatModelOption);
43
+ if (args?.includeEmbedding) command.addOption(embedModelOption);
44
+ if (args?.includeSearch) command.addOption(indexNameOption);
64
45
 
65
- // Azure Search options
66
- if (args?.includeSearch === true) {
67
- command.addOption(azureSearchEndpointOption);
68
- command.addOption(azureSearchApiKeyOption);
69
- command.addOption(azureSearchIndexNameOption);
70
- }
71
-
72
- // Validation hook
73
46
  command.hook('preAction', (thisCommand) => {
74
- const options = thisCommand.opts();
47
+ const opts = thisCommand.opts();
75
48
 
76
- // Validate API key
77
- if (
78
- !options.openaiApiKey ||
79
- typeof options.openaiApiKey !== 'string' ||
80
- options.openaiApiKey.trim() === ''
81
- ) {
49
+ if (args?.includeChat && !opts.chatModel?.trim()) {
82
50
  throw new InvalidOptionArgumentError(
83
- 'Azure OpenAI API key is required. Provide it via --openai-api-key option or AZURE_OPENAI_API_KEY environment variable.',
51
+ 'Chat model name is required. Provide --chat-model or set FUSION_AI_CHAT_MODEL.',
84
52
  );
85
53
  }
86
-
87
- // Validate API version
88
- if (!options.openaiApiVersion || typeof options.openaiApiVersion !== 'string') {
89
- throw new InvalidOptionArgumentError('API version must be a non-empty string.');
90
- }
91
-
92
- // Validate instance name
93
- if (
94
- !options.openaiInstance ||
95
- typeof options.openaiInstance !== 'string' ||
96
- options.openaiInstance.trim() === ''
97
- ) {
54
+ if (args?.includeEmbedding && !opts.embedModel?.trim()) {
98
55
  throw new InvalidOptionArgumentError(
99
- 'Azure OpenAI instance name is required. Provide it via --openai-instance option or AZURE_OPENAI_INSTANCE_NAME environment variable.',
56
+ 'Embedding model name is required. Provide --embed-model or set FUSION_AI_EMBED_MODEL.',
100
57
  );
101
58
  }
102
-
103
- if (args?.includeChat === true) {
104
- if (
105
- !options.openaiChatDeployment ||
106
- typeof options.openaiChatDeployment !== 'string' ||
107
- options.openaiChatDeployment.trim() === ''
108
- ) {
109
- throw new InvalidOptionArgumentError(
110
- 'Chat deployment name is required and must be a non-empty string.',
111
- );
112
- }
113
- }
114
-
115
- if (args?.includeEmbedding === true) {
116
- if (
117
- !options.openaiEmbeddingDeployment ||
118
- typeof options.openaiEmbeddingDeployment !== 'string' ||
119
- options.openaiEmbeddingDeployment.trim() === ''
120
- ) {
121
- throw new InvalidOptionArgumentError(
122
- 'Azure OpenAI embedding deployment name is required. Provide it via --openai-embedding-deployment option or AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME environment variable.',
123
- );
124
- }
125
- }
126
-
127
- if (args?.includeSearch === true) {
128
- if (
129
- !options.azureSearchEndpoint ||
130
- typeof options.azureSearchEndpoint !== 'string' ||
131
- options.azureSearchEndpoint.trim() === ''
132
- ) {
133
- throw new InvalidOptionArgumentError(
134
- 'Azure Search endpoint is required and must be a non-empty string.',
135
- );
136
- }
137
-
138
- if (
139
- !options.azureSearchApiKey ||
140
- typeof options.azureSearchApiKey !== 'string' ||
141
- options.azureSearchApiKey.trim() === ''
142
- ) {
143
- throw new InvalidOptionArgumentError(
144
- 'Azure Search API key is required. Provide it via --azure-search-api-key option or AZURE_SEARCH_API_KEY environment variable.',
145
- );
146
- }
147
-
148
- if (
149
- !options.azureSearchIndexName ||
150
- typeof options.azureSearchIndexName !== 'string' ||
151
- options.azureSearchIndexName.trim() === ''
152
- ) {
153
- throw new InvalidOptionArgumentError(
154
- 'Azure Search index name is required and must be a non-empty string.',
155
- );
156
- }
59
+ if (args?.includeSearch && !opts.indexName?.trim()) {
60
+ throw new InvalidOptionArgumentError(
61
+ 'Index name is required. Provide --index-name or set FUSION_AI_INDEX_NAME.',
62
+ );
157
63
  }
158
64
  });
159
65
 
@@ -1,107 +1,102 @@
1
- import { enableAI, type IAIConfigurator, type AIModule } from '@equinor/fusion-framework-module-ai';
2
-
3
- import {
4
- AzureOpenAiEmbed,
5
- AzureOpenAIModel,
6
- AzureVectorStore,
7
- } from '@equinor/fusion-framework-module-ai/azure';
1
+ import { enableAI } from '@equinor/fusion-framework-module-ai';
2
+ import type { AIModule } from '@equinor/fusion-framework-module-ai';
8
3
 
4
+ import { initializeFramework, FusionEnv } from '@equinor/fusion-framework-cli/bin';
5
+ import type { FusionFrameworkSettings, FusionFramework } from '@equinor/fusion-framework-cli/bin';
9
6
  import type { AiOptions } from './options/index.js';
10
- import { ModulesConfigurator, type ModulesInstance } from '@equinor/fusion-framework-module';
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+
10
+ /** Initialized framework instance with the AI module. */
11
+ export type FrameworkInstance = FusionFramework<[AIModule]>;
11
12
 
12
13
  /**
13
- * Framework instance with AI module capabilities.
14
+ * Check whether an error (possibly wrapped in a cause chain) is an
15
+ * authentication-related failure that may be recoverable via interactive login.
14
16
  *
15
- * This type represents an initialized Fusion Framework instance that includes
16
- * the AI module, providing access to chat models, embedding services, and
17
- * vector stores configured via the setup process.
17
+ * @internal
18
18
  */
19
- export type FrameworkInstance = ModulesInstance<[AIModule]>;
19
+ const isAuthError = (error: unknown): boolean => {
20
+ let current: unknown = error;
21
+ while (current) {
22
+ if (current instanceof Error) {
23
+ if (
24
+ current.name === 'NoAccountsError' ||
25
+ current.name === 'SilentTokenAcquisitionError' ||
26
+ current.message.includes('No accounts found')
27
+ ) {
28
+ return true;
29
+ }
30
+ }
31
+ current = (current as { cause?: unknown }).cause;
32
+ }
33
+ return false;
34
+ };
20
35
 
21
36
  /**
22
- * Initializes and configures the Fusion Framework with AI module capabilities.
37
+ * Creates a Fusion Framework instance with the AI module enabled.
23
38
  *
24
- * Sets up the framework with Azure OpenAI chat models, embedding services, and
25
- * optionally Azure Cognitive Search vector stores. The function handles the complete
26
- * initialization process including service registration and dependency injection.
39
+ * Initialises the Fusion Framework with service discovery and MSAL auth,
40
+ * resolves the `'ai'` service endpoint, and pre-caches a bearer token.
41
+ * If MSAL has no cached credentials, the CLI's interactive `auth login`
42
+ * flow is spawned automatically before retrying.
27
43
  *
28
- * @param options - AI configuration options
29
- * @param options.openaiApiKey - Azure OpenAI API key for authentication
30
- * @param options.openaiApiVersion - Azure OpenAI API version (e.g., '2024-02-15-preview')
31
- * @param options.openaiInstance - Azure OpenAI instance name
32
- * @param options.openaiChatDeployment - Optional chat model deployment name
33
- * @param options.openaiEmbeddingDeployment - Optional embedding model deployment name
34
- * @param options.azureSearchEndpoint - Optional Azure Search service endpoint URL
35
- * @param options.azureSearchApiKey - Optional Azure Search API key
36
- * @param options.azureSearchIndexName - Optional Azure Search index name
37
- * @returns Promise resolving to an initialized framework instance with AI module configured
38
- * @throws {Error} If embedding deployment is required but not provided when configuring vector store
39
- * @throws {Error} If embedding service cannot be retrieved for vector store configuration
44
+ * @param options - CLI options resolved by {@link withOptions}.
45
+ * @returns A fully initialised framework instance with the AI module.
46
+ * @throws {Error} When authentication fails after the interactive retry.
40
47
  */
41
- export const setupFramework = async (options: AiOptions): Promise<FrameworkInstance> => {
42
- // Create a new module configurator for the framework
43
- const configurator = new ModulesConfigurator<[AIModule]>();
48
+ export const setupFramework = async (options: AiOptions): Promise<FusionFramework<[AIModule]>> => {
49
+ // Service-discovery mode: resolve URL + scopes from Fusion service registry
50
+ const auth: FusionFrameworkSettings['auth'] = options.token
51
+ ? { token: options.token }
52
+ : {
53
+ tenantId: options.tenantId ?? '3aa4a235-b6e2-48d5-9195-7fcf05b459b0',
54
+ clientId: options.clientId ?? 'a318b8e1-0295-4e17-98d5-35f67dfeba14',
55
+ };
44
56
 
45
- // Configure AI module with provided options
46
- enableAI(configurator, (aiConfig: IAIConfigurator) => {
47
- // Configure chat model if deployment name is provided
48
- if (options.openaiChatDeployment) {
49
- aiConfig.setModel(
50
- options.openaiChatDeployment,
51
- new AzureOpenAIModel({
52
- azureOpenAIApiKey: options.openaiApiKey,
53
- azureOpenAIApiDeploymentName: options.openaiChatDeployment,
54
- azureOpenAIApiInstanceName: options.openaiInstance,
55
- azureOpenAIApiVersion: options.openaiApiVersion,
56
- }),
57
- );
58
- }
57
+ const env = (options.env as FusionEnv) ?? FusionEnv.ContinuesIntegration;
59
58
 
60
- // Configure embedding model if deployment name is provided
61
- if (options.openaiEmbeddingDeployment) {
62
- aiConfig.setEmbedding(
63
- options.openaiEmbeddingDeployment,
64
- new AzureOpenAiEmbed({
65
- azureOpenAIApiKey: options.openaiApiKey,
66
- azureOpenAIApiDeploymentName: options.openaiEmbeddingDeployment,
67
- azureOpenAIApiInstanceName: options.openaiInstance,
68
- azureOpenAIApiVersion: options.openaiApiVersion,
69
- }),
70
- );
71
- }
59
+ /** Initialise the framework, resolve the AI service, and pre-cache tokens. */
60
+ const initAndSetup = async (): Promise<FusionFramework<[AIModule]>> => {
61
+ const framework = await initializeFramework<[AIModule]>({ env, auth }, (configurator) => {
62
+ enableAI(configurator);
63
+ });
72
64
 
73
- // Configure vector store if Azure Search options are provided
74
- // Vector store requires an embedding service to generate embeddings for documents
75
- if (options.azureSearchEndpoint && options.azureSearchApiKey && options.azureSearchIndexName) {
76
- if (!options.openaiEmbeddingDeployment) {
77
- throw new Error('Embedding deployment is required to configure the vector store');
78
- }
65
+ // resolveService makes an authenticated HTTP call will throw
66
+ // NoAccountsError if the user has never logged in.
67
+ const service = await framework.serviceDiscovery.resolveService('ai');
68
+ const scopes = service.scopes ?? service.defaultScopes ?? [];
79
69
 
80
- // Retrieve the embedding service to pass to the vector store
81
- // The vector store uses embeddings to index and search documents
82
- const embeddingService = aiConfig.getService('embeddings', options.openaiEmbeddingDeployment);
70
+ // Pre-cache a token for the AI service scopes so strategy callbacks
71
+ // don't attempt (and fail) a silent acquisition later.
72
+ const token = await framework.auth.acquireAccessToken({ request: { scopes } });
73
+ if (!token) throw new Error('Failed to acquire access token for the AI service.');
83
74
 
84
- // Check that the embedding service was successfully retrieved
85
- if (!embeddingService) {
86
- throw new Error(
87
- `Embedding service '${options.openaiEmbeddingDeployment}' not found for vector store configuration`,
88
- );
89
- }
75
+ return framework;
76
+ };
77
+
78
+ try {
79
+ return await initAndSetup();
80
+ } catch (error: unknown) {
81
+ // If the failure is auth-related and we're not using a static token,
82
+ // spawn the CLI's own `auth login` (starts local server + browser)
83
+ // and retry the full init sequence.
84
+ if (!isAuthError(error) || options.token) throw error;
90
85
 
91
- aiConfig.setVectorStore(
92
- options.azureSearchIndexName,
93
- new AzureVectorStore(embeddingService, {
94
- endpoint: options.azureSearchEndpoint,
95
- key: options.azureSearchApiKey,
96
- indexName: options.azureSearchIndexName,
97
- }),
86
+ const cliEntry = process.argv[1];
87
+ if (!cliEntry) {
88
+ throw new Error(
89
+ 'Failed to acquire access token and could not determine CLI path for interactive login.',
98
90
  );
99
91
  }
100
- });
101
92
 
102
- // Initialize the framework with all configured modules
103
- const framework = await configurator.initialize();
104
- return framework;
93
+ console.log('No cached credentials launching interactive login…');
94
+ execFileSync(process.execPath, [cliEntry, 'auth', 'login'], {
95
+ stdio: 'inherit',
96
+ });
97
+
98
+ return await initAndSetup();
99
+ }
105
100
  };
106
101
 
107
102
  export default setupFramework;
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '2.0.1';
2
+ export const version = '3.0.0';
package/tsconfig.json CHANGED
@@ -9,6 +9,9 @@
9
9
  "baseUrl": "."
10
10
  },
11
11
  "references": [
12
+ {
13
+ "path": "../../cli"
14
+ },
12
15
  {
13
16
  "path": "../../modules/ai"
14
17
  },