refactoring_v1 (#391)
This commit is contained in:
+24
-3
@@ -1,7 +1,28 @@
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import AnthropicClient from '@anthropic-ai/sdk';
|
||||
import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
|
||||
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
|
||||
import { AxiosInstance as RawAxiosClient } from 'axios';
|
||||
import { OpenAI as OpenAIClient } from 'openai';
|
||||
|
||||
export interface AiEngineConfig {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
maxTokensOutput: number;
|
||||
maxTokensInput: number;
|
||||
baseURL?: string;
|
||||
}
|
||||
|
||||
type Client =
|
||||
| OpenAIClient
|
||||
| AzureOpenAIClient
|
||||
| AnthropicClient
|
||||
| RawAxiosClient
|
||||
| GeminiClient;
|
||||
|
||||
export interface AiEngine {
|
||||
config: AiEngineConfig;
|
||||
client: Client;
|
||||
generateCommitMessage(
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined>;
|
||||
messages: Array<OpenAIClient.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
+28
-70
@@ -1,104 +1,62 @@
|
||||
import AnthropicClient from '@anthropic-ai/sdk';
|
||||
import {
|
||||
MessageCreateParamsNonStreaming,
|
||||
MessageParam
|
||||
} from '@anthropic-ai/sdk/resources/messages.mjs';
|
||||
import { outro } from '@clack/prompts';
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import {ChatCompletionRequestMessage} from 'openai'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { OpenAI } from 'openai';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
const config = getConfig();
|
||||
interface AnthropicConfig extends AiEngineConfig {}
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
export class AnthropicEngine implements AiEngine {
|
||||
config: AnthropicConfig;
|
||||
client: AnthropicClient;
|
||||
|
||||
let provider = config?.OCO_AI_PROVIDER;
|
||||
let apiKey = config?.OCO_ANTHROPIC_API_KEY;
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
if (
|
||||
provider === 'anthropic' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_ANTHROPIC_API_KEY is not set, please run `oco config set OCO_ANTHROPIC_API_KEY=<your token> . If you are using Claude, make sure you add payment details, so API works.`'
|
||||
);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL;
|
||||
if (provider === 'anthropic' &&
|
||||
typeof MODEL !== 'string' &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL}. The model can be any string, but the current configuration is not supported.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export class AnthropicAi implements AiEngine {
|
||||
private anthropicAiApiConfiguration = {
|
||||
apiKey: apiKey
|
||||
};
|
||||
private anthropicAI!: Anthropic;
|
||||
|
||||
constructor() {
|
||||
this.anthropicAI = new Anthropic(this.anthropicAiApiConfiguration);
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.client = new AnthropicClient({ apiKey: this.config.apiKey });
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> => {
|
||||
|
||||
const systemMessage = messages.find(msg => msg.role === 'system')?.content as string;
|
||||
const restMessages = messages.filter((msg) => msg.role !== 'system') as MessageParam[];
|
||||
const systemMessage = messages.find((msg) => msg.role === 'system')
|
||||
?.content as string;
|
||||
const restMessages = messages.filter(
|
||||
(msg) => msg.role !== 'system'
|
||||
) as MessageParam[];
|
||||
|
||||
const params: MessageCreateParamsNonStreaming = {
|
||||
model: MODEL,
|
||||
model: this.config.model,
|
||||
system: systemMessage,
|
||||
messages: restMessages,
|
||||
temperature: 0,
|
||||
top_p: 0.1,
|
||||
max_tokens: MAX_TOKENS_OUTPUT
|
||||
max_tokens: this.config.maxTokensOutput
|
||||
};
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
if (
|
||||
REQUEST_TOKENS >
|
||||
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||
) {
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const data = await this.anthropicAI.messages.create(params);
|
||||
const data = await this.client.messages.create(params);
|
||||
|
||||
const message = data?.content[0].text;
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
|
||||
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
|
||||
|
||||
+32
-64
@@ -1,81 +1,51 @@
|
||||
import {
|
||||
AzureKeyCredential,
|
||||
OpenAIClient as AzureOpenAIClient
|
||||
} from '@azure/openai';
|
||||
import { outro } from '@clack/prompts';
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
} from 'openai';
|
||||
|
||||
import { OpenAIClient, AzureKeyCredential } from '@azure/openai';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { OpenAI } from 'openai';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
||||
let apiKey = config?.OCO_AZURE_API_KEY;
|
||||
let apiEndpoint = config?.OCO_AZURE_ENDPOINT;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
|
||||
if (
|
||||
provider === 'azure' &&
|
||||
!apiKey &&
|
||||
!apiEndpoint &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_AZURE_API_KEY or OCO_AZURE_ENDPOINT are not set, please run `oco config set OCO_AZURE_API_KEY=<your token> . If you are using GPT, make sure you add payment details, so API works.`'
|
||||
);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
interface AzureAiEngineConfig extends AiEngineConfig {
|
||||
baseURL: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
||||
export class AzureEngine implements AiEngine {
|
||||
config: AzureAiEngineConfig;
|
||||
client: AzureOpenAIClient;
|
||||
|
||||
export class Azure implements AiEngine {
|
||||
private openAI!: OpenAIClient;
|
||||
|
||||
constructor() {
|
||||
if (provider === 'azure') {
|
||||
this.openAI = new OpenAIClient(apiEndpoint, new AzureKeyCredential(apiKey));
|
||||
}
|
||||
constructor(config: AzureAiEngineConfig) {
|
||||
this.config = config;
|
||||
this.client = new AzureOpenAIClient(
|
||||
this.config.baseURL,
|
||||
new AzureKeyCredential(this.config.apiKey)
|
||||
);
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
generateCommitMessage = async (
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content) + 4)
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
if (
|
||||
REQUEST_TOKENS >
|
||||
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||
) {
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const data = await this.openAI.getChatCompletions(MODEL, messages);
|
||||
const data = await this.client.getChatCompletions(
|
||||
this.config.model,
|
||||
messages
|
||||
);
|
||||
|
||||
const message = data.choices[0].message;
|
||||
|
||||
@@ -84,10 +54,10 @@ export class Azure implements AiEngine {
|
||||
}
|
||||
return message?.content;
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${MODEL}`);
|
||||
outro(`${chalk.red('✖')} ${this.config.model}`);
|
||||
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
outro(`${chalk.red('✖')} ${JSON.stringify(error)}`);
|
||||
|
||||
if (
|
||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||
@@ -105,5 +75,3 @@ export class Azure implements AiEngine {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const azure = new Azure();
|
||||
|
||||
+28
-26
@@ -1,38 +1,40 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { AiEngine } from './Engine';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { OpenAI } from 'openai';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
import {
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
interface FlowiseAiConfig extends AiEngineConfig {}
|
||||
|
||||
export class FlowiseAi implements AiEngine {
|
||||
config: FlowiseAiConfig;
|
||||
client: AxiosInstance;
|
||||
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.client = axios.create({
|
||||
url: `${config.baseURL}/${config.apiKey}`,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
async generateCommitMessage(
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> {
|
||||
const gitDiff = (messages[messages.length - 1]?.content as string)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t');
|
||||
|
||||
const gitDiff = messages[ messages.length - 1 ]?.content?.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t');
|
||||
const url = `http://${config?.OCO_FLOWISE_ENDPOINT}/api/v1/prediction/${config?.OCO_FLOWISE_API_KEY}`;
|
||||
const payload = {
|
||||
question : gitDiff,
|
||||
overrideConfig : {
|
||||
systemMessagePrompt: messages[0]?.content,
|
||||
},
|
||||
history : messages.slice( 1, -1 )
|
||||
}
|
||||
question: gitDiff,
|
||||
overrideConfig: {
|
||||
systemMessagePrompt: messages[0]?.content
|
||||
},
|
||||
history: messages.slice(1, -1)
|
||||
};
|
||||
try {
|
||||
const response = await axios.post(url, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const response = await this.client.post('', payload);
|
||||
const message = response.data;
|
||||
return message?.text;
|
||||
} catch (err: any) {
|
||||
|
||||
+52
-97
@@ -1,133 +1,88 @@
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { AiEngine } from './Engine';
|
||||
import { Content, GenerativeModel, GoogleGenerativeAI, HarmBlockThreshold, HarmCategory, Part } from '@google/generative-ai';
|
||||
import { CONFIG_MODES, ConfigType, DEFAULT_TOKEN_LIMITS, getConfig, MODEL_LIST } from '../commands/config';
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
Content,
|
||||
GoogleGenerativeAI,
|
||||
HarmBlockThreshold,
|
||||
HarmCategory,
|
||||
Part
|
||||
} from '@google/generative-ai';
|
||||
import axios from 'axios';
|
||||
import { OpenAI } from 'openai';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
interface GeminiConfig extends AiEngineConfig {}
|
||||
|
||||
export class Gemini implements AiEngine {
|
||||
config: GeminiConfig;
|
||||
client: GoogleGenerativeAI;
|
||||
|
||||
private readonly config: ConfigType;
|
||||
private readonly googleGenerativeAi: GoogleGenerativeAI;
|
||||
private ai: GenerativeModel;
|
||||
|
||||
// vars
|
||||
private maxTokens = {
|
||||
input: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT,
|
||||
output: DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT
|
||||
};
|
||||
private basePath: string;
|
||||
private apiKey: string;
|
||||
private model: string;
|
||||
|
||||
constructor() {
|
||||
this.config = getConfig() as ConfigType;
|
||||
this.googleGenerativeAi = new GoogleGenerativeAI(this.config.OCO_GEMINI_API_KEY);
|
||||
|
||||
this.warmup();
|
||||
constructor(config) {
|
||||
this.client = new GoogleGenerativeAI(config.apiKey);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async generateCommitMessage(messages: ChatCompletionRequestMessage[]): Promise<string | undefined> {
|
||||
const systemInstruction = messages.filter(m => m.role === 'system')
|
||||
.map(m => m.content)
|
||||
async generateCommitMessage(
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> {
|
||||
const systemInstruction = messages
|
||||
.filter((m) => m.role === 'system')
|
||||
.map((m) => m.content)
|
||||
.join('\n');
|
||||
|
||||
this.ai = this.googleGenerativeAi.getGenerativeModel({
|
||||
model: this.model,
|
||||
systemInstruction,
|
||||
|
||||
const gemini = this.client.getGenerativeModel({
|
||||
model: this.config.model,
|
||||
systemInstruction
|
||||
});
|
||||
|
||||
const contents = messages.filter(m => m.role !== 'system')
|
||||
.map(m => ({ parts: [{ text: m.content } as Part], role: m.role == 'user' ? m.role : 'model', } as Content));
|
||||
|
||||
|
||||
const contents = messages
|
||||
.filter((m) => m.role !== 'system')
|
||||
.map(
|
||||
(m) =>
|
||||
({
|
||||
parts: [{ text: m.content } as Part],
|
||||
role: m.role === 'user' ? m.role : 'model'
|
||||
} as Content)
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.ai.generateContent({
|
||||
contents,
|
||||
const result = await gemini.generateContent({
|
||||
contents,
|
||||
safetySettings: [
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE
|
||||
}
|
||||
],
|
||||
generationConfig: {
|
||||
maxOutputTokens: this.maxTokens.output,
|
||||
maxOutputTokens: this.config.maxTokensOutput,
|
||||
temperature: 0,
|
||||
topP: 0.1,
|
||||
},
|
||||
topP: 0.1
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return result.response.text();
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
|
||||
if (
|
||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||
error.response?.status === 401
|
||||
) {
|
||||
const geminiError = error.response.data.error;
|
||||
|
||||
if (geminiError?.message) outro(geminiError.message);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
if (geminiError) throw new Error(geminiError?.message);
|
||||
}
|
||||
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private warmup(): void {
|
||||
if (this.config.OCO_TOKENS_MAX_INPUT !== undefined) this.maxTokens.input = this.config.OCO_TOKENS_MAX_INPUT;
|
||||
if (this.config.OCO_TOKENS_MAX_OUTPUT !== undefined) this.maxTokens.output = this.config.OCO_TOKENS_MAX_OUTPUT;
|
||||
this.basePath = this.config.OCO_GEMINI_BASE_PATH;
|
||||
this.apiKey = this.config.OCO_GEMINI_API_KEY;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const provider = this.config.OCO_AI_PROVIDER;
|
||||
|
||||
if (provider === 'gemini' && !this.apiKey &&
|
||||
command !== 'config' && mode !== 'set') {
|
||||
intro('opencommit');
|
||||
|
||||
outro('OCO_GEMINI_API_KEY is not set, please run `oco config set OCO_GEMINI_API_KEY=<your token> . If you are using GPT, make sure you add payment details, so API works.');
|
||||
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.model = this.config.OCO_MODEL || MODEL_LIST.gemini[0];
|
||||
|
||||
if (provider === 'gemini' &&
|
||||
!MODEL_LIST.gemini.includes(this.model) &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${this.model} for Gemini. Supported models are: ${MODEL_LIST.gemini.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+20
-31
@@ -1,52 +1,41 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { AiEngine } from './Engine';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { OpenAI } from 'openai';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
import {
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
interface OllamaConfig extends AiEngineConfig {}
|
||||
|
||||
export class OllamaAi implements AiEngine {
|
||||
private model = "mistral"; // as default model of Ollama
|
||||
private url = "http://localhost:11434/api/chat"; // default URL of Ollama API
|
||||
config: OllamaConfig;
|
||||
client: AxiosInstance;
|
||||
|
||||
setModel(model: string) {
|
||||
this.model = model ?? config?.OCO_MODEL ?? 'mistral';
|
||||
}
|
||||
|
||||
setUrl(url: string) {
|
||||
this.url = url ?? config?.OCO_OLLAMA_API_URL ?? 'http://localhost:11434/api/chat';
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.client = axios.create({
|
||||
url: config.baseURL
|
||||
? `${config.baseURL}/${config.apiKey}`
|
||||
: 'http://localhost:11434/api/chat',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
async generateCommitMessage(
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> {
|
||||
const model = this.model;
|
||||
|
||||
//console.log(messages);
|
||||
//process.exit()
|
||||
|
||||
const url = this.url;
|
||||
const p = {
|
||||
model,
|
||||
const params = {
|
||||
model: this.config.model ?? 'mistral',
|
||||
messages,
|
||||
options: { temperature: 0, top_p: 0.1 },
|
||||
stream: false
|
||||
};
|
||||
try {
|
||||
const response = await axios.post(url, p, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const response = await this.client.post('', params);
|
||||
|
||||
const message = response.data.message;
|
||||
|
||||
return message?.content;
|
||||
} catch (err: any) {
|
||||
const message = err.response?.data?.error ?? err.message;
|
||||
throw new Error('local model issues. details: ' + message);
|
||||
throw new Error(`Ollama provider error: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-89
@@ -1,127 +1,59 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
Configuration as OpenAiApiConfiguration,
|
||||
OpenAIApi
|
||||
} from 'openai';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { OpenAI } from 'openai';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
import { AiEngine, AiEngineConfig } from './Engine';
|
||||
|
||||
const config = getConfig();
|
||||
interface OpenAiConfig extends AiEngineConfig {}
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
||||
let apiKey = config?.OCO_OPENAI_API_KEY;
|
||||
export class OpenAiEngine implements AiEngine {
|
||||
config: OpenAiConfig;
|
||||
client: OpenAI;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
|
||||
if (
|
||||
provider === 'openai' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_OPENAI_API_KEY is not set, please run `oco config set OCO_OPENAI_API_KEY=<your token> . If you are using GPT, make sure you add payment details, so API works.`'
|
||||
);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
||||
if (provider === 'openai' &&
|
||||
typeof MODEL !== 'string' &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL}. The model can be any string, but the current configuration is not supported.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export class OpenAi implements AiEngine {
|
||||
|
||||
private openAiApiConfiguration = new OpenAiApiConfiguration({
|
||||
apiKey: apiKey
|
||||
});
|
||||
private openAI!: OpenAIApi;
|
||||
|
||||
constructor() {
|
||||
if (basePath) {
|
||||
this.openAiApiConfiguration.basePath = basePath;
|
||||
}
|
||||
this.openAI = new OpenAIApi(this.openAiApiConfiguration);
|
||||
constructor(config: OpenAiConfig) {
|
||||
this.config = config;
|
||||
this.client = new OpenAI({ apiKey: config.apiKey });
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined> => {
|
||||
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | null> => {
|
||||
const params = {
|
||||
model: MODEL,
|
||||
model: this.config.model,
|
||||
messages,
|
||||
temperature: 0,
|
||||
top_p: 0.1,
|
||||
max_tokens: MAX_TOKENS_OUTPUT
|
||||
max_tokens: this.config.maxTokensOutput
|
||||
};
|
||||
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
if (
|
||||
REQUEST_TOKENS >
|
||||
this.config.maxTokensInput - this.config.maxTokensOutput
|
||||
)
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const { data } = await this.openAI.createChatCompletion(params);
|
||||
const completion = await this.client.chat.completions.create(params);
|
||||
|
||||
const message = data.choices[0].message;
|
||||
const message = completion.choices[0].message;
|
||||
|
||||
return message?.content;
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
|
||||
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
|
||||
if (
|
||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||
error.response?.status === 401
|
||||
) {
|
||||
const openAiError = error.response.data.error;
|
||||
|
||||
if (openAiError?.message) outro(openAiError.message);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
if (openAiError) throw new Error(openAiError.message);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
+28
-12
@@ -1,31 +1,47 @@
|
||||
import { ChatCompletionRequestMessage } from 'openai';
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
import { AiEngine } from './Engine';
|
||||
import { getConfig } from '../commands/config';
|
||||
|
||||
export const TEST_MOCK_TYPES = [
|
||||
'commit-message',
|
||||
'prompt-module-commitlint-config',
|
||||
] as const
|
||||
type TestMockType = typeof TEST_MOCK_TYPES[number];
|
||||
'prompt-module-commitlint-config'
|
||||
] as const;
|
||||
|
||||
export type TestMockType = (typeof TEST_MOCK_TYPES)[number];
|
||||
|
||||
type TestAiEngine = Partial<AiEngine> & {
|
||||
mockType: TestMockType;
|
||||
};
|
||||
|
||||
export class TestAi implements TestAiEngine {
|
||||
mockType: TestMockType;
|
||||
|
||||
// those are not used in the test engine
|
||||
config: any;
|
||||
client: any;
|
||||
// ---
|
||||
|
||||
constructor(mockType: TestMockType) {
|
||||
this.mockType = mockType;
|
||||
}
|
||||
|
||||
export class TestAi implements AiEngine {
|
||||
async generateCommitMessage(
|
||||
_messages: Array<ChatCompletionRequestMessage>
|
||||
_messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
|
||||
): Promise<string | undefined> {
|
||||
const config = getConfig();
|
||||
switch (config?.OCO_TEST_MOCK_TYPE as TestMockType | undefined) {
|
||||
switch (this.mockType) {
|
||||
case 'commit-message':
|
||||
return 'fix(testAi.ts): test commit message';
|
||||
case 'prompt-module-commitlint-config':
|
||||
return `{\n` +
|
||||
return (
|
||||
`{\n` +
|
||||
` "localLanguage": "english",\n` +
|
||||
` "commitFix": "fix(server): Change 'port' variable to uppercase 'PORT'",\n` +
|
||||
` "commitFeat": "feat(server): Allow server to listen on a port specified through environment variable",\n` +
|
||||
` "commitDescription": "Change 'port' variable to uppercase 'PORT'. Allow server to listen on a port specified through environment variable."\n` +
|
||||
`}`
|
||||
);
|
||||
default:
|
||||
throw Error('unsupported test mock type')
|
||||
throw Error('unsupported test mock type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user