Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/src/application/useCases/GenerateContentUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GeminiService } from '../../infrastructure/external/GeminiService';
import { CustomFunctionService } from '../../domain/services/CustomFunctionService';

export class GenerateContentUseCase {
constructor(
private readonly geminiService: GeminiService,
private readonly customFunctionService: CustomFunctionService
) {}

async execute(prompt: string, functionName?: string): Promise<string> {
if (functionName) {
const customFunction = await this.customFunctionService.getFunction(functionName);
if (customFunction) {
prompt = `${prompt}\n\nUse the following custom function:\n${JSON.stringify(customFunction, null, 2)}`;
}
}

return await this.geminiService.generateContent(prompt);
}
}
12 changes: 12 additions & 0 deletions app/src/domain/entities/CustomFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface CustomFunction {
name: string;
description: string;
parameters: CustomFunctionParameter[];
}

export interface CustomFunctionParameter {
name: string;
type: string;
description: string;
required: boolean;
}
6 changes: 6 additions & 0 deletions app/src/domain/repositories/ICustomFunctionRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CustomFunction } from '../entities/CustomFunction';

export interface ICustomFunctionRepository {
save(customFunction: CustomFunction): Promise<void>;
getByName(name: string): Promise<CustomFunction | null>;
}
14 changes: 14 additions & 0 deletions app/src/domain/services/CustomFunctionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CustomFunction } from '../entities/CustomFunction';
import { ICustomFunctionRepository } from '../repositories/ICustomFunctionRepository';

export class CustomFunctionService {
constructor(private readonly repository: ICustomFunctionRepository) {}

async registerFunction(customFunction: CustomFunction): Promise<void> {
await this.repository.save(customFunction);
}

async getFunction(name: string): Promise<CustomFunction | null> {
return await this.repository.getByName(name);
}
}
33 changes: 33 additions & 0 deletions app/src/infrastructure/external/GeminiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { fetcher } from '../../lib/fetcher';
import { cache } from '../../lib/cache';
import { generateHash } from '../../utils/generateHash';
import { generateUrl } from '../../utils/generateUrl';

export class GeminiService {
constructor(
private readonly apiKey: string,
private readonly baseUrl: string = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'
) {}

async generateContent(prompt: string): Promise<string> {
const cacheKey = generateHash(prompt);
const cachedResponse = cache.get(cacheKey);

if (cachedResponse) {
return cachedResponse;
}

const url = generateUrl(this.baseUrl, { key: this.apiKey });
const response = await fetcher.post(url, {
contents: [{
parts: [{
text: prompt
}]
}]
});

const result = response.candidates[0].content.parts[0].text;
cache.set(cacheKey, result);
return result;
}
}
20 changes: 20 additions & 0 deletions app/src/infrastructure/repositories/CustomFunctionRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CustomFunction } from '../../domain/entities/CustomFunction';
import { ICustomFunctionRepository } from '../../domain/repositories/ICustomFunctionRepository';
import { Properties } from '../../lib/properties';

export class CustomFunctionRepository implements ICustomFunctionRepository {
constructor(private readonly properties: Properties) {}

async save(customFunction: CustomFunction): Promise<void> {
const functions = this.properties.getProperty('customFunctions') || '{}';
const existingFunctions = JSON.parse(functions);
existingFunctions[customFunction.name] = customFunction;
await this.properties.setProperty('customFunctions', JSON.stringify(existingFunctions));
}

async getByName(name: string): Promise<CustomFunction | null> {
const functions = this.properties.getProperty('customFunctions') || '{}';
const existingFunctions = JSON.parse(functions);
return existingFunctions[name] || null;
}
}
14 changes: 14 additions & 0 deletions app/src/interfaces/api/CustomFunctionController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CustomFunction } from '../../domain/entities/CustomFunction';
import { CustomFunctionService } from '../../domain/services/CustomFunctionService';

export class CustomFunctionController {
constructor(private readonly customFunctionService: CustomFunctionService) {}

async registerFunction(request: CustomFunction): Promise<void> {
await this.customFunctionService.registerFunction(request);
}

async getFunction(name: string): Promise<CustomFunction | null> {
return await this.customFunctionService.getFunction(name);
}
}
83 changes: 18 additions & 65 deletions app/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,18 @@
import { geminiService } from '@/services'
import { cache as libCache, properties } from '@/lib'
import { generateHash } from '@/utils'
import { GEMINI_API_KEY_SCRIPT_PROPERTY } from '@/config'

global.onOpen = () => {
// カスタムメニューを追加
const ui = SpreadsheetApp.getUi()
ui.createMenu('GEMINI').addItem('APIキーを登録する', 'showApiKeyInputForm').addToUi()
}

/**
* APIキーを入力するためのダイアログを表示します。
*/
global.showApiKeyInputForm = () => {
const html = HtmlService.createHtmlOutputFromFile('static/settingApiKey')
.setWidth(400)
.setHeight(200)
SpreadsheetApp.getUi().showModalDialog(html, 'APIキーを入力してください。')
}

/**
* APIキーを保存します。
*/
global.saveApiKey = (apiKey: string) => {
properties.set(GEMINI_API_KEY_SCRIPT_PROPERTY, apiKey)
}

/**
* GEMNI API に質問をできるカスタム関数です。
* @param {string} contents 質問内容
* @param {string} referenceCell 参照セル
* @return {string} GEMINI API からの回答
* @customfunction
*/
global.GEMINI = (contents = '岐阜県の県庁所在地', referenceCell = '') => {
if (Array.isArray(referenceCell)) {
throw Error('複数のセルに対する参照は対応しておりません。')
}

const prompt = `${contents}:${referenceCell}`

const cacheKey = `gemini:${generateHash(prompt)}`
const cache = libCache(cacheKey)
const cacheValue = cache.get()

Logger.log(cacheValue)

if (cacheValue) {
return cacheValue
}

const payload = geminiService.createGeminiPayLoad(prompt)

const response = geminiService.requestGemini(payload)

const text = geminiService.getResponseText(response)

if (text) {
cache.set(text)
return text
} else {
return '回答がありません。'
}
}
import { Properties } from './lib/properties';
import { CustomFunctionRepository } from './infrastructure/repositories/CustomFunctionRepository';
import { CustomFunctionService } from './domain/services/CustomFunctionService';
import { GeminiService } from './infrastructure/external/GeminiService';
import { GenerateContentUseCase } from './application/useCases/GenerateContentUseCase';
import { CustomFunctionController } from './interfaces/api/CustomFunctionController';

const properties = new Properties();
const customFunctionRepository = new CustomFunctionRepository(properties);
const customFunctionService = new CustomFunctionService(customFunctionRepository);
const geminiService = new GeminiService(process.env.GEMINI_API_KEY || '');
const generateContentUseCase = new GenerateContentUseCase(geminiService, customFunctionService);
const customFunctionController = new CustomFunctionController(customFunctionService);

export {
generateContentUseCase,
customFunctionController
};