From bb1bee2e84b52b7b45bf5ed2f5628b74a701b527 Mon Sep 17 00:00:00 2001 From: sakaritoru <129810356+sakaritoru@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:41:28 +0900 Subject: [PATCH] refactor: implement DDD architecture directory structure --- .../useCases/GenerateContentUseCase.ts | 20 +++++ app/src/domain/entities/CustomFunction.ts | 12 +++ .../repositories/ICustomFunctionRepository.ts | 6 ++ .../domain/services/CustomFunctionService.ts | 14 ++++ .../infrastructure/external/GeminiService.ts | 33 ++++++++ .../repositories/CustomFunctionRepository.ts | 20 +++++ .../api/CustomFunctionController.ts | 14 ++++ app/src/main.ts | 83 ++++--------------- 8 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 app/src/application/useCases/GenerateContentUseCase.ts create mode 100644 app/src/domain/entities/CustomFunction.ts create mode 100644 app/src/domain/repositories/ICustomFunctionRepository.ts create mode 100644 app/src/domain/services/CustomFunctionService.ts create mode 100644 app/src/infrastructure/external/GeminiService.ts create mode 100644 app/src/infrastructure/repositories/CustomFunctionRepository.ts create mode 100644 app/src/interfaces/api/CustomFunctionController.ts diff --git a/app/src/application/useCases/GenerateContentUseCase.ts b/app/src/application/useCases/GenerateContentUseCase.ts new file mode 100644 index 0000000..5b318b3 --- /dev/null +++ b/app/src/application/useCases/GenerateContentUseCase.ts @@ -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 { + 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); + } +} \ No newline at end of file diff --git a/app/src/domain/entities/CustomFunction.ts b/app/src/domain/entities/CustomFunction.ts new file mode 100644 index 0000000..deb7292 --- /dev/null +++ b/app/src/domain/entities/CustomFunction.ts @@ -0,0 +1,12 @@ +export interface CustomFunction { + name: string; + description: string; + parameters: CustomFunctionParameter[]; +} + +export interface CustomFunctionParameter { + name: string; + type: string; + description: string; + required: boolean; +} \ No newline at end of file diff --git a/app/src/domain/repositories/ICustomFunctionRepository.ts b/app/src/domain/repositories/ICustomFunctionRepository.ts new file mode 100644 index 0000000..424f1cf --- /dev/null +++ b/app/src/domain/repositories/ICustomFunctionRepository.ts @@ -0,0 +1,6 @@ +import { CustomFunction } from '../entities/CustomFunction'; + +export interface ICustomFunctionRepository { + save(customFunction: CustomFunction): Promise; + getByName(name: string): Promise; +} \ No newline at end of file diff --git a/app/src/domain/services/CustomFunctionService.ts b/app/src/domain/services/CustomFunctionService.ts new file mode 100644 index 0000000..5d12d71 --- /dev/null +++ b/app/src/domain/services/CustomFunctionService.ts @@ -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 { + await this.repository.save(customFunction); + } + + async getFunction(name: string): Promise { + return await this.repository.getByName(name); + } +} \ No newline at end of file diff --git a/app/src/infrastructure/external/GeminiService.ts b/app/src/infrastructure/external/GeminiService.ts new file mode 100644 index 0000000..147483b --- /dev/null +++ b/app/src/infrastructure/external/GeminiService.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/app/src/infrastructure/repositories/CustomFunctionRepository.ts b/app/src/infrastructure/repositories/CustomFunctionRepository.ts new file mode 100644 index 0000000..a828e9d --- /dev/null +++ b/app/src/infrastructure/repositories/CustomFunctionRepository.ts @@ -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 { + 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 { + const functions = this.properties.getProperty('customFunctions') || '{}'; + const existingFunctions = JSON.parse(functions); + return existingFunctions[name] || null; + } +} \ No newline at end of file diff --git a/app/src/interfaces/api/CustomFunctionController.ts b/app/src/interfaces/api/CustomFunctionController.ts new file mode 100644 index 0000000..102fc5e --- /dev/null +++ b/app/src/interfaces/api/CustomFunctionController.ts @@ -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 { + await this.customFunctionService.registerFunction(request); + } + + async getFunction(name: string): Promise { + return await this.customFunctionService.getFunction(name); + } +} \ No newline at end of file diff --git a/app/src/main.ts b/app/src/main.ts index 91b36a8..6a0dc7d 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -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 +}; \ No newline at end of file