diff --git a/.vscode/settings.json b/.vscode/settings.json index b5c0c11bc..631ad10b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,6 @@ "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, - "**/node_modules": true, }, "search.exclude": { "**/lib": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b3b2182..820b573e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta` +- LLM: Add HuggingFace integration, using LangchainJS provider + ## [5.39.1] 2025-06-05 - [hardis:doc:project2markdown](https://sfdx-hardis.cloudity.com/hardis/doc/project2markdown/): Define DO_NOT_OVERWRITE_INDEX_MD=true to avoid overwriting the index.md file in docs folder, useful if you want to keep your own index.md file. diff --git a/docs/salesforce-ai-setup.md b/docs/salesforce-ai-setup.md index a60d00456..8242e8000 100644 --- a/docs/salesforce-ai-setup.md +++ b/docs/salesforce-ai-setup.md @@ -66,11 +66,12 @@ Currently supported LangchainJS providers: - OpenAI - Anthropic - Google GenAI +- HuggingFace | Variable | Description | Default | |-----------------------------|-------------------------------------------------------------------------------------------------|----------------------------------| | USE_LANGCHAIN_LLM | Set to true to use LangChain integration | `false` | -| LANGCHAIN_LLM_PROVIDER | The LLM provider to use (currently supports `ollama`, `openai`, `anthropic` and `google-genai`) | | +| LANGCHAIN_LLM_PROVIDER | The LLM provider to use (currently supports `ollama`, `openai`, `anthropic`, `google-genai` and `huggingface`) | | | LANGCHAIN_LLM_MODEL | The model to use with the selected provider (e.g. `gpt-4o`, `qwen2.5-coder:14b`) | | | LANGCHAIN_LLM_MODEL_API_KEY | API key for the selected provider (required for OpenAI, Anthropic and Gemini) | | | LANGCHAIN_LLM_TEMPERATURE | Controls randomness (0-1) | | @@ -125,6 +126,25 @@ LANGCHAIN_LLM_MODEL=gemini-1.5-pro LANGCHAIN_LLM_MODEL_API_KEY=your-api-key ``` +For HuggingFace: + +- Create an account at [HuggingFace](https://huggingface.co/) +- Get your API token from [HuggingFace Tokens page](https://huggingface.co/settings/tokens) +- Choose from thousands of available models on the [HuggingFace Model Hub](https://huggingface.co/models) + +```sh +USE_LANGCHAIN_LLM=true +LANGCHAIN_LLM_PROVIDER=huggingface +LANGCHAIN_LLM_MODEL=microsoft/DialoGPT-medium +LANGCHAIN_LLM_MODEL_API_KEY=your-huggingface-token +``` + +Popular HuggingFace models you can use: +- `microsoft/DialoGPT-medium` - Conversational AI model +- `google/flan-t5-large` - Text-to-text generation model +- `EleutherAI/gpt-neo-2.7B` - GPT-like language model +- `facebook/blenderbot-400M-distill` - Conversational AI model + ### With OpenAI Directly You need to define env variable OPENAI_API_KEY and make it available to your CI/CD workflow. diff --git a/docs/salesforce-project-doc-ai.md b/docs/salesforce-project-doc-ai.md index 10d8737d0..3d43a3382 100644 --- a/docs/salesforce-project-doc-ai.md +++ b/docs/salesforce-project-doc-ai.md @@ -24,6 +24,6 @@ If AI Integration is configured, the following parts of the documentation with b - Lightning Web Components - Lightning Pages -Configure AI integration following the [related documentation](salesforce-ai-setup.md) +Configure AI integration following [**AI Setup documentation**](salesforce-ai-setup.md) See the [list of prompts used by sfdx-hardis](salesforce-ai-prompts.md) to enhance documentation with AI, and how to override them. diff --git a/package.json b/package.json index 0020ae27d..360c4528f 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "@actions/github": "^6.0.1", "@cparra/apexdocs": "^3.12.1", "@gitbeaker/node": "^35.8.1", + "@huggingface/inference": "^4.0.2", "@langchain/anthropic": "^0.3.21", - "@langchain/community": "^0.3.44", + "@langchain/community": "^0.3.45", "@langchain/core": "^0.3.57", "@langchain/google-genai": "^0.2.10", "@langchain/ollama": "^0.2.0", diff --git a/src/common/aiProvider/langChainProviders/langChainAnthropicProvider.ts b/src/common/aiProvider/langChainProviders/langChainAnthropicProvider.ts index 9cb4df645..020c06fcd 100644 --- a/src/common/aiProvider/langChainProviders/langChainAnthropicProvider.ts +++ b/src/common/aiProvider/langChainProviders/langChainAnthropicProvider.ts @@ -1,6 +1,5 @@ import { ChatAnthropic } from "@langchain/anthropic"; -import { BaseChatModel } from "@langchain/core/language_models/chat_models"; -import { AbstractLLMProvider, ModelConfig } from "./langChainBaseProvider.js"; +import { AbstractLLMProvider, ModelConfig, SupportedModel } from "./langChainBaseProvider.js"; export class LangChainAnthropicProvider extends AbstractLLMProvider { constructor(modelName: string, config: ModelConfig) { @@ -11,15 +10,15 @@ export class LangChainAnthropicProvider extends AbstractLLMProvider { this.model = this.getModel(); } - getModel(): BaseChatModel { + getModel(): SupportedModel { const config = { - modelName: this.modelName, - anthropicApiKey: this.config.apiKey!, + model: this.modelName, + apiKey: this.config.apiKey!, temperature: this.config.temperature, maxTokens: this.config.maxTokens, maxRetries: this.config.maxRetries }; - return new ChatAnthropic(config) as BaseChatModel; + return new ChatAnthropic(config); } } \ No newline at end of file diff --git a/src/common/aiProvider/langChainProviders/langChainBaseProvider.ts b/src/common/aiProvider/langChainProviders/langChainBaseProvider.ts index 2f1bb55d8..10dd68968 100644 --- a/src/common/aiProvider/langChainProviders/langChainBaseProvider.ts +++ b/src/common/aiProvider/langChainProviders/langChainBaseProvider.ts @@ -1,4 +1,5 @@ import { BaseChatModel } from "@langchain/core/language_models/chat_models"; +import { LLM } from "@langchain/core/language_models/llms"; export interface ModelConfig { temperature?: number; @@ -9,16 +10,19 @@ export interface ModelConfig { apiKey?: string; } -export type ProviderType = "ollama" | "openai" | "anthropic"; +export type ProviderType = "ollama" | "openai" | "anthropic" | "google-genai" | "huggingface"; + +// Union type to support both chat models and LLMs +export type SupportedModel = BaseChatModel | LLM; export interface BaseLLMProvider { - getModel(): BaseChatModel; + getModel(): SupportedModel; getModelName(): string; getLabel(): string; } export abstract class AbstractLLMProvider implements BaseLLMProvider { - protected model: BaseChatModel; + protected model: SupportedModel; protected modelName: string; protected config: ModelConfig; @@ -27,8 +31,8 @@ export abstract class AbstractLLMProvider implements BaseLLMProvider { this.config = config; } - abstract getModel(): BaseChatModel; - + abstract getModel(): SupportedModel; + getModelName(): string { return this.modelName; } @@ -36,4 +40,4 @@ export abstract class AbstractLLMProvider implements BaseLLMProvider { getLabel(): string { return "LangChain connector"; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/common/aiProvider/langChainProviders/langChainGoogleGenAi.ts b/src/common/aiProvider/langChainProviders/langChainGoogleGenAi.ts index 19316c30e..6c784be5f 100644 --- a/src/common/aiProvider/langChainProviders/langChainGoogleGenAi.ts +++ b/src/common/aiProvider/langChainProviders/langChainGoogleGenAi.ts @@ -1,6 +1,5 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; -import { BaseChatModel } from "@langchain/core/language_models/chat_models"; -import { AbstractLLMProvider, ModelConfig } from "./langChainBaseProvider.js"; +import { AbstractLLMProvider, ModelConfig, SupportedModel } from "./langChainBaseProvider.js"; export class LangChainGoogleGenAiProvider extends AbstractLLMProvider { constructor(modelName: string, config: ModelConfig) { @@ -11,7 +10,7 @@ export class LangChainGoogleGenAiProvider extends AbstractLLMProvider { this.model = this.getModel(); } - getModel(): BaseChatModel { + getModel(): SupportedModel { const config = { model: this.modelName, apiKey: this.config.apiKey!, @@ -20,6 +19,6 @@ export class LangChainGoogleGenAiProvider extends AbstractLLMProvider { maxRetries: this.config.maxRetries }; - return new ChatGoogleGenerativeAI(config) as BaseChatModel; + return new ChatGoogleGenerativeAI(config); } } \ No newline at end of file diff --git a/src/common/aiProvider/langChainProviders/langChainHuggingFaceProvider.ts b/src/common/aiProvider/langChainProviders/langChainHuggingFaceProvider.ts new file mode 100644 index 000000000..2fc80fbf9 --- /dev/null +++ b/src/common/aiProvider/langChainProviders/langChainHuggingFaceProvider.ts @@ -0,0 +1,32 @@ +import { HuggingFaceInference } from "@langchain/community/llms/hf"; +import { AbstractLLMProvider, ModelConfig, SupportedModel } from "./langChainBaseProvider.js"; +import { getEnvVar } from "../../../config/index.js"; + +export class LangChainHuggingFaceProvider extends AbstractLLMProvider { + constructor(modelName: string, config: ModelConfig) { + if (!config.apiKey) { + throw new Error("API key is required for HuggingFace provider. Define it in a secured env var LANGCHAIN_LLM_MODEL_API_KEY"); + } + super(modelName, config); + this.model = this.getModel(); + } + + getModel(): SupportedModel { + const config = { + model: this.modelName, + apiKey: this.config.apiKey!, + temperature: this.config.temperature, + maxTokens: this.config.maxTokens, + // HuggingFace specific configuration + endpointUrl: this.config.baseUrl, // Custom endpoint URL if needed + options: { + provider: getEnvVar("HF_INFERENCE_PROVIDER") || "default", + } + }; + return new HuggingFaceInference(config); + } + + getLabel(): string { + return "HuggingFace LangChain connector"; + } +} diff --git a/src/common/aiProvider/langChainProviders/langChainOllamaProvider.ts b/src/common/aiProvider/langChainProviders/langChainOllamaProvider.ts index 467973987..f1f6a58ad 100644 --- a/src/common/aiProvider/langChainProviders/langChainOllamaProvider.ts +++ b/src/common/aiProvider/langChainProviders/langChainOllamaProvider.ts @@ -1,6 +1,5 @@ import { ChatOllama } from "@langchain/ollama"; -import { BaseChatModel } from "@langchain/core/language_models/chat_models"; -import { AbstractLLMProvider, ModelConfig } from "./langChainBaseProvider.js"; +import { AbstractLLMProvider, ModelConfig, SupportedModel } from "./langChainBaseProvider.js"; export class LangChainOllamaProvider extends AbstractLLMProvider { constructor(modelName: string, config: ModelConfig) { @@ -8,7 +7,7 @@ export class LangChainOllamaProvider extends AbstractLLMProvider { this.model = this.getModel(); } - getModel(): BaseChatModel { + getModel(): SupportedModel { const config = { model: this.modelName, baseUrl: this.config.baseUrl || "http://localhost:11434", @@ -16,6 +15,6 @@ export class LangChainOllamaProvider extends AbstractLLMProvider { maxRetries: this.config.maxRetries }; - return new ChatOllama(config) as BaseChatModel; + return new ChatOllama(config); } } \ No newline at end of file diff --git a/src/common/aiProvider/langChainProviders/langChainOpenAIProvider.ts b/src/common/aiProvider/langChainProviders/langChainOpenAIProvider.ts index 1f95407a6..f6df69ba3 100644 --- a/src/common/aiProvider/langChainProviders/langChainOpenAIProvider.ts +++ b/src/common/aiProvider/langChainProviders/langChainOpenAIProvider.ts @@ -1,6 +1,5 @@ import { ChatOpenAI } from "@langchain/openai"; -import { BaseChatModel } from "@langchain/core/language_models/chat_models"; -import { AbstractLLMProvider, ModelConfig } from "./langChainBaseProvider.js"; +import { AbstractLLMProvider, ModelConfig, SupportedModel } from "./langChainBaseProvider.js"; export class LangChainOpenAIProvider extends AbstractLLMProvider { constructor(modelName: string, config: ModelConfig) { @@ -11,15 +10,15 @@ export class LangChainOpenAIProvider extends AbstractLLMProvider { this.model = this.getModel(); } - getModel(): BaseChatModel { + getModel(): SupportedModel { const config = { - modelName: this.modelName, - openAIApiKey: this.config.apiKey!, + model: this.modelName, + apiKey: this.config.apiKey!, temperature: this.config.temperature, maxTokens: this.config.maxTokens, maxRetries: this.config.maxRetries }; - return new ChatOpenAI(config) as BaseChatModel; + return new ChatOpenAI(config); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/common/aiProvider/langChainProviders/langChainProviderFactory.ts b/src/common/aiProvider/langChainProviders/langChainProviderFactory.ts index 6e60b07a1..1bb5dcab4 100644 --- a/src/common/aiProvider/langChainProviders/langChainProviderFactory.ts +++ b/src/common/aiProvider/langChainProviders/langChainProviderFactory.ts @@ -3,8 +3,9 @@ import { LangChainOllamaProvider } from "./langChainOllamaProvider.js"; import { LangChainOpenAIProvider } from "./langChainOpenAIProvider.js"; import { LangChainAnthropicProvider } from "./langChainAnthropicProvider.js"; import { LangChainGoogleGenAiProvider } from "./langChainGoogleGenAi.js"; +import { LangChainHuggingFaceProvider } from "./langChainHuggingFaceProvider.js"; -const ALL_PROVIDERS = ["ollama", "openai", "anthropic", "google-genai"]; +const ALL_PROVIDERS = ["ollama", "openai", "anthropic", "google-genai", "huggingface"]; export class LangChainProviderFactory { static createProvider(providerType: ProviderType, modelName: string, config: ModelConfig): BaseLLMProvider { @@ -17,6 +18,8 @@ export class LangChainProviderFactory { return new LangChainAnthropicProvider(modelName, config); case "google-genai": return new LangChainGoogleGenAiProvider(modelName, config); + case "huggingface": + return new LangChainHuggingFaceProvider(modelName, config); default: throw new Error(`Unsupported LLM provider: ${providerType}. Supported providers are: ${ALL_PROVIDERS.join(", ")}`); } diff --git a/src/common/aiProvider/langchainProvider.ts b/src/common/aiProvider/langchainProvider.ts index a2d5d6c8e..25a8dfc6f 100644 --- a/src/common/aiProvider/langchainProvider.ts +++ b/src/common/aiProvider/langchainProvider.ts @@ -1,4 +1,5 @@ import { BaseChatModel } from "@langchain/core/language_models/chat_models"; +import { LLM } from "@langchain/core/language_models/llms"; import { AiResponse } from "./index.js"; import { AiProviderRoot } from "./aiProviderRoot.js"; import c from "chalk"; @@ -6,10 +7,10 @@ import { uxLog } from "../utils/index.js"; import { PromptTemplate } from "./promptTemplates.js"; import { getEnvVar } from "../../config/index.js"; import { LangChainProviderFactory } from "./langChainProviders/langChainProviderFactory.js"; -import { ModelConfig, ProviderType } from "./langChainProviders/langChainBaseProvider.js"; +import { ModelConfig, ProviderType, SupportedModel } from "./langChainProviders/langChainBaseProvider.js"; export class LangChainProvider extends AiProviderRoot { - private model: BaseChatModel; + private model: SupportedModel; private modelName: string; constructor() { @@ -22,11 +23,11 @@ export class LangChainProvider extends AiProviderRoot { const providerType = provider.toLowerCase() as ProviderType; const modelName = getEnvVar("LANGCHAIN_LLM_MODEL"); const apiKey = getEnvVar("LANGCHAIN_LLM_MODEL_API_KEY"); - + if (!modelName) { throw new Error("LANGCHAIN_LLM_MODEL environment variable must be set to use LangChain integration"); } - + this.modelName = modelName; // Common configuration for all providers @@ -47,7 +48,6 @@ export class LangChainProvider extends AiProviderRoot { public getLabel(): string { return "LangChain connector"; } - public async promptAi(promptText: string, template: PromptTemplate | null = null): Promise { // re-use the same check for max ai calls number as in the original openai provider implementation if (!this.checkMaxAiCallsNumber()) { @@ -65,12 +65,23 @@ export class LangChainProvider extends AiProviderRoot { this.incrementAiCallsNumber(); try { - const response = await this.model.invoke([ - { - role: "user", - content: promptText - } - ]); + let response: any; + + // Check if the model is a BaseChatModel or LLM and call accordingly + if (this.model instanceof BaseChatModel) { + // For chat models, use message format + response = await this.model.invoke([ + { + role: "user", + content: promptText + } + ]); + } else if (this.model instanceof LLM) { + // For LLMs, use plain string + response = await this.model.invoke(promptText); + } else { + throw new Error("Unsupported model type"); + } if (process.env?.DEBUG_PROMPTS === "true") { uxLog(this, c.grey("[LangChain] Received prompt response\n" + JSON.stringify(response, null, 2))); @@ -83,9 +94,17 @@ export class LangChainProvider extends AiProviderRoot { model: this.modelName, }; - if (response.content) { + // Handle different response formats + let responseContent: string | undefined; + if (this.model instanceof BaseChatModel && response.content) { + responseContent = typeof response.content === 'string' ? response.content : JSON.stringify(response.content); + } else if (this.model instanceof LLM && typeof response === 'string') { + responseContent = response; + } + + if (responseContent) { aiResponse.success = true; - aiResponse.promptResponse = typeof response.content === 'string' ? response.content : JSON.stringify(response.content); + aiResponse.promptResponse = responseContent; } return aiResponse; diff --git a/yarn.lock b/yarn.lock index 09a1cd146..68dd914f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1433,6 +1433,24 @@ protobufjs "^7.2.5" yargs "^17.7.2" +"@huggingface/inference@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@huggingface/inference/-/inference-4.0.2.tgz#962a14c468311755fa34da143c045c34b9dd7775" + integrity sha512-XuWb8ocH7lA5kSdXrGnqshtRz3ocSBzEzxcp5xeAXLjgM1ocoIHq+RW8/Ti0xq3MeRGQWgUkYPCgDV/xgs8p4g== + dependencies: + "@huggingface/jinja" "^0.5.0" + "@huggingface/tasks" "^0.19.11" + +"@huggingface/jinja@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.5.0.tgz#0da65deb98798cd24ea42ad13f6df224ce23f443" + integrity sha512-Ptc03/jGRiYRoi0bUYKZ14MkDslsBRT24oxmsvUlfYrvQMldrxCevhPnT+hfX8awKTT8/f/0ZBBWldoeAcMHdQ== + +"@huggingface/tasks@^0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@huggingface/tasks/-/tasks-0.19.11.tgz#2c1005b189432528c1e17d1ec9938d11f65a01ca" + integrity sha512-oBhSgVlg7Pp643MsH8BiI3OAXIMJNxdSiMtv4mApRZV8dmAz8oasKhg6CVKIplO7vAO7F6dkmMn4bYM64I2A9w== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -1893,10 +1911,10 @@ zod "^3.22.4" zod-to-json-schema "^3.22.4" -"@langchain/community@^0.3.44": - version "0.3.44" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.44.tgz#6048e357d2168de975f1b33d54e91105059f86cc" - integrity sha512-lOA7rw0lC6WCRO/xoacx4Gpbx1ncscAilYn9LVjyiBxJw47d01iq8hdkGdBW5OFISub/wCK4FmHih2S4WJicAg== +"@langchain/community@^0.3.45": + version "0.3.45" + resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.45.tgz#7af0cfd67ee662bf4f88a655babbf808918262fd" + integrity sha512-KkAGmnP+w5tozLYsj/kGKwyfuPnCcA6MyDXfNF7oDo7L1TxhUgdEKhvNsY7ooLXz6Xh/LV5Kqp2B8U0jfYCQKQ== dependencies: "@langchain/openai" ">=0.2.0 <0.6.0" "@langchain/weaviate" "^0.2.0"