-
Notifications
You must be signed in to change notification settings - Fork 0
feat: profiles cli api #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e86c3a3
Base profiles
michaelroudnitski 02ec84d
Base profile commands
michaelroudnitski df6bacc
Install openai langchain
michaelroudnitski 242b126
Update markdown command to require profiles, better mocking
michaelroudnitski 0dbe609
Use newest granite models in examples
michaelroudnitski 8d23cbe
Reject path traversal
michaelroudnitski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import {Args, Command} from '@oclif/core' | ||
|
|
||
| import {deleteProfile} from '../../lib/profile/storage.js' | ||
|
|
||
| export default class ProfilesDelete extends Command { | ||
| static args = { | ||
| name: Args.string({ | ||
| description: 'Profile name to delete', | ||
| required: true, | ||
| }), | ||
| } | ||
| static description = 'Delete a profile' | ||
| static examples = ['<%= config.bin %> <%= command.id %> my-profile'] | ||
|
|
||
| async run(): Promise<void> { | ||
| const {args} = await this.parse(ProfilesDelete) | ||
|
|
||
| try { | ||
| deleteProfile(args.name) | ||
| this.log(`Profile "${args.name}" deleted successfully`) | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| this.error(error.message) | ||
| } | ||
|
|
||
| throw error | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import {Command} from '@oclif/core' | ||
|
|
||
| import {listProfiles} from '../../lib/profile/storage.js' | ||
|
|
||
| export default class ProfilesList extends Command { | ||
| static description = 'List all profiles' | ||
| static examples = ['<%= config.bin %> <%= command.id %>'] | ||
|
|
||
| async run(): Promise<void> { | ||
| const profiles = listProfiles() | ||
|
|
||
| if (profiles.length === 0) { | ||
| this.log('No profiles found') | ||
| return | ||
| } | ||
|
|
||
| for (const profile of profiles) { | ||
| this.log(profile) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import {Args, Command, Flags} from '@oclif/core' | ||
|
|
||
| import type {OpenAIProfileConfig, WatsonxProfileConfig} from '../../lib/profile/types.js' | ||
|
|
||
| import {saveProfile} from '../../lib/profile/storage.js' | ||
|
|
||
| export default class ProfilesSet extends Command { | ||
| static args = { | ||
| name: Args.string({ | ||
| description: 'Profile name', | ||
| required: true, | ||
| }), | ||
| } | ||
| static description = 'Create or update a profile' | ||
| static examples = [ | ||
| '<%= config.bin %> <%= command.id %> my-openai-profile --provider openai --api-key sk-... --model gpt-4o', | ||
| '<%= config.bin %> <%= command.id %> my-watsonx-profile --provider watsonx --api-key ... --project-id ... --service-url https://... --model ibm/granite-4-h-small', | ||
| ] | ||
| static flags = { | ||
| 'api-key': Flags.string({ | ||
| description: 'API key for the provider', | ||
| required: true, | ||
| }), | ||
| model: Flags.string({ | ||
| description: 'Model to use', | ||
| required: true, | ||
| }), | ||
| 'project-id': Flags.string({ | ||
| description: 'Watsonx project ID (required for watsonx)', | ||
| required: false, | ||
| }), | ||
| provider: Flags.string({ | ||
| description: 'LLM provider', | ||
| options: ['openai', 'watsonx'], | ||
| required: true, | ||
| }), | ||
| 'service-url': Flags.string({ | ||
| description: 'Watsonx service URL (required for watsonx)', | ||
| required: false, | ||
| }), | ||
| } | ||
|
|
||
| async run(): Promise<void> { | ||
| const {args, flags} = await this.parse(ProfilesSet) | ||
|
|
||
| if (flags.provider === 'openai') { | ||
| const config: OpenAIProfileConfig = { | ||
| apiKey: flags['api-key'], | ||
| model: flags.model, | ||
| } | ||
|
|
||
| saveProfile({ | ||
| config, | ||
| name: args.name, | ||
| provider: 'openai', | ||
| }) | ||
|
|
||
| this.log(`Profile "${args.name}" saved successfully`) | ||
| } else if (flags.provider === 'watsonx') { | ||
| if (!flags['project-id']) { | ||
| this.error('--project-id is required for watsonx provider') | ||
| } | ||
|
|
||
| if (!flags['service-url']) { | ||
| this.error('--service-url is required for watsonx provider') | ||
| } | ||
|
|
||
| const config: WatsonxProfileConfig = { | ||
| apiKey: flags['api-key'], | ||
| model: flags.model, | ||
| projectId: flags['project-id'], | ||
| serviceUrl: flags['service-url'], | ||
| } | ||
|
|
||
| saveProfile({ | ||
| config, | ||
| name: args.name, | ||
| provider: 'watsonx', | ||
| }) | ||
|
|
||
| this.log(`Profile "${args.name}" saved successfully`) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import {FakeListChatModel} from '@langchain/core/utils/testing' | ||
|
|
||
| export interface FakeConfig { | ||
| responses: string[] | ||
| } | ||
|
|
||
| export function createClient(config: FakeConfig): FakeListChatModel { | ||
| return new FakeListChatModel({ | ||
| responses: config.responses, | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import {ChatOpenAI} from '@langchain/openai' | ||
|
|
||
| export interface OpenAIConfig { | ||
| apiKey: string | ||
| maxTokens?: number | ||
| model: string | ||
| temperature?: number | ||
| } | ||
|
|
||
| export function createClient(config: OpenAIConfig): ChatOpenAI { | ||
| return new ChatOpenAI({ | ||
| apiKey: config.apiKey, | ||
| maxRetries: 3, | ||
| maxTokens: config.maxTokens || 2000, | ||
| model: config.model, | ||
| temperature: config.temperature || 0.3, | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,49 +1,24 @@ | ||
| import {ChatWatsonx} from '@langchain/community/chat_models/ibm' | ||
|
|
||
| /** | ||
| * Configuration for watsonx.ai client | ||
| */ | ||
| export interface WatsonxConfig { | ||
| apiKey: string | ||
| maxNewTokens?: number | ||
| model?: string | ||
| model: string | ||
| projectId: string | ||
| serviceUrl: string | ||
| temperature?: number | ||
| } | ||
|
|
||
| /** | ||
| * Creates and returns a configured watsonx.ai chat model with IAM authentication | ||
| */ | ||
| export function createClient(config?: Partial<WatsonxConfig>): ChatWatsonx { | ||
| // Get configuration from environment variables or provided config | ||
| const serviceUrl = config?.serviceUrl || process.env.WATSONX_AI_SERVICE_URL | ||
| const projectId = config?.projectId || process.env.WATSONX_AI_PROJECT_ID | ||
| const apiKey = config?.apiKey || process.env.WATSONX_AI_APIKEY | ||
|
|
||
| // Validate required configuration | ||
| if (!serviceUrl) { | ||
| throw new Error('WATSONX_AI_SERVICE_URL is required') | ||
| } | ||
|
|
||
| if (!projectId) { | ||
| throw new Error('WATSONX_AI_PROJECT_ID is required') | ||
| } | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error('WATSONX_AI_APIKEY is required') | ||
| } | ||
|
|
||
| // Create and return the chat model with IAM authentication | ||
| export function createClient(config: WatsonxConfig): ChatWatsonx { | ||
| return new ChatWatsonx({ | ||
| maxRetries: 3, | ||
| maxTokens: config?.maxNewTokens || 2000, | ||
| model: config?.model || 'ibm/granite-4-h-small', | ||
| projectId, | ||
| serviceUrl, | ||
| temperature: config?.temperature || 0.3, | ||
| maxTokens: config.maxNewTokens || 2000, | ||
| model: config.model, | ||
| projectId: config.projectId, | ||
| serviceUrl: config.serviceUrl, | ||
| temperature: config.temperature || 0.3, | ||
| version: '2024-05-31', | ||
| watsonxAIApikey: apiKey, | ||
| watsonxAIApikey: config.apiKey, | ||
| watsonxAIAuthType: 'iam', | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type {BaseChatModel} from '@langchain/core/language_models/chat_models' | ||
|
|
||
| import type {Profile} from './types.js' | ||
|
|
||
| import {createClient as createFakeClient} from '../../core/providers/fake.js' | ||
| import {createClient as createOpenAIClient} from '../../core/providers/openai.js' | ||
| import {createClient as createWatsonxClient} from '../../core/providers/watsonx.js' | ||
|
|
||
| export function createProviderFromProfile(profile: Profile): BaseChatModel { | ||
| switch (profile.provider) { | ||
| case 'fake': { | ||
| return createFakeClient(profile.config) | ||
| } | ||
|
|
||
| case 'openai': { | ||
| return createOpenAIClient(profile.config) | ||
| } | ||
|
|
||
| case 'watsonx': { | ||
| return createWatsonxClient(profile.config) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import {existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync} from 'node:fs' | ||
| import {homedir} from 'node:os' | ||
| import {join} from 'node:path' | ||
|
|
||
| import type {Profile} from './types.js' | ||
|
|
||
| export function getProfilesDir(): string { | ||
| const home = process.env.HOME || homedir() | ||
| return join(home, '.config', 'translation-ai-cli', 'profiles') | ||
| } | ||
|
|
||
| export function ensureProfilesDir(): void { | ||
| const dir = getProfilesDir() | ||
| if (!existsSync(dir)) { | ||
| mkdirSync(dir, {recursive: true}) | ||
| } | ||
| } | ||
|
|
||
| export function getProfilePath(name: string): string { | ||
| // Reject path traversal attempts, empty names, or names with path separators | ||
| if (!name || name.includes('/') || name.includes('\\') || name.includes('..')) { | ||
| throw new Error(`Invalid profile name: "${name}". Profile names cannot contain path separators or parent directory references.`) | ||
| } | ||
|
|
||
| return join(getProfilesDir(), `${name}.json`) | ||
| } | ||
|
|
||
| export function saveProfile(profile: Profile): void { | ||
| ensureProfilesDir() | ||
| const path = getProfilePath(profile.name) | ||
| writeFileSync(path, JSON.stringify(profile, null, 2), { | ||
| encoding: 'utf8', | ||
| mode: 0o600, // only owner can read/write | ||
| }) | ||
| } | ||
|
|
||
| export function loadProfile(name: string): Profile { | ||
| const path = getProfilePath(name) | ||
| if (!existsSync(path)) { | ||
| throw new Error(`Profile "${name}" does not exist`) | ||
| } | ||
|
|
||
| const content = readFileSync(path, 'utf8') | ||
| return JSON.parse(content) as Profile | ||
| } | ||
|
|
||
| export function listProfiles(): string[] { | ||
| const dir = getProfilesDir() | ||
| if (!existsSync(dir)) { | ||
| return [] | ||
| } | ||
|
|
||
| return readdirSync(dir) | ||
| .filter((file) => file.endsWith('.json')) | ||
| .map((file) => file.replace(/\.json$/, '')) | ||
| } | ||
|
|
||
| export function deleteProfile(name: string): void { | ||
| const path = getProfilePath(name) | ||
| if (!existsSync(path)) { | ||
| throw new Error(`Profile "${name}" does not exist`) | ||
| } | ||
|
|
||
| rmSync(path) | ||
| } | ||
|
|
||
| export function profileExists(name: string): boolean { | ||
| return existsSync(getProfilePath(name)) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.