Skip to content
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

feature/add guardian image api #227

Merged
merged 7 commits into from
Sep 21, 2024
Merged
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
28 changes: 14 additions & 14 deletions apps/api/src/features/guardians/guardian.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { handleError } from '@peace-net/shared/utils/error-handler'
import { Hono } from 'hono'

import { getEnv } from '~/config/environment'
import { GuardianController } from '~/features/guardians/guardian.controller'
import { GuardianService } from '~/features/guardians/guardian.service'
import { GuardianUseCase } from '~/features/guardians/guardian.usecase'
import { UsageLogRepository } from '~/features/usageLogs/usageLog.repository'
import { UsageLogService } from '~/features/usageLogs/usageLog.service'
import { UsageFacade } from '~/features/usages/usage.facade'
Expand All @@ -18,11 +15,18 @@ import { UserPlanService } from '~/features/userPlans/userPlan.service'
import { AnthropicClient, GoogleClient, OpenAIClient } from '~/libs/models'
import { SupabaseClient } from '~/libs/supabase'

import { GuardianImageController } from './guardianImage.controller'
import { GuardianImageService } from './guardianImage.service'
import { GuardianImageUseCase } from './guardianImage.usecase'
import { GuardianTextController } from './guardianText.controller'
import { GuardianTextService } from './guardianText.service'
import { GuardianTextUseCase } from './guardianText.usecase'

/**
* Guardian APIのルーティングを定義します
* テキストと画像のコンテンツモデレーション用エンドポイントを提供
* - POST /text: テキスト分析
* - POST /image: 画像分析(未実装)
* - POST /image: 画像分析
*/
const guardianRoutes = new Hono()

Expand All @@ -35,9 +39,9 @@ guardianRoutes.post(
return
}),
async (c) => {
return new GuardianController(
new GuardianUseCase(
new GuardianService(
return new GuardianTextController(
new GuardianTextUseCase(
new GuardianTextService(
OpenAIClient(getEnv(c).OPENAI_API_KEY),
AnthropicClient(getEnv(c).ANTHROPIC_API_KEY),
GoogleClient(getEnv(c).GOOGLE_API_KEY),
Expand Down Expand Up @@ -74,13 +78,9 @@ guardianRoutes.post(
return
}),
async (c) => {
return new GuardianController(
new GuardianUseCase(
new GuardianService(
OpenAIClient(getEnv(c).OPENAI_API_KEY),
AnthropicClient(getEnv(c).ANTHROPIC_API_KEY),
GoogleClient(getEnv(c).GOOGLE_API_KEY),
),
return new GuardianImageController(
new GuardianImageUseCase(
new GuardianImageService(OpenAIClient(getEnv(c).OPENAI_API_KEY)),
new UsageFacade(
new UserPlanService(
new UserPlanRepository(
Expand Down
37 changes: 37 additions & 0 deletions apps/api/src/features/guardians/guardianImage.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { GuardianImageDTO } from '@peace-net/shared/types/guardian'
import { Context } from 'hono'

import { IGuardianImageUseCase } from './guardianImage.usecase'

/**
* 画像の不適切な内容を分析し、カテゴリー別のスコアを提供するコントローラー
*
* このコントローラーは、GuardianUseCaseを使用してテキストの不適切な内容を分析し、結果をクライアントに返します。
*
* @class GuardianImageController
* @method guardianImage - 指定された画像を分析し、不適切な内容の有無を判定します
* @param guardianImageUseCase - GuardianUseCaseのインスタンス
* @returns 分析結果を含むJSONレスポンス
* @throws エラーが発生した場合はエラーレスポンスを返します
*/
export class GuardianImageController {
constructor(private guardianImageUseCase: IGuardianImageUseCase) {}

async guardianImage(c: Context) {
const dto = (await c.req.json()) as GuardianImageDTO

const userId = c.get('userId') as string
const apiKeyId = c.get('apiKeyId') as string
const result = await this.guardianImageUseCase.guardianImage({
...dto,
userId,
apiKeyId,
})

if (!result.ok) {
throw result.error
}

return c.json(result.value)
}
}
80 changes: 80 additions & 0 deletions apps/api/src/features/guardians/guardianImage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { OpenAIProvider } from '@ai-sdk/openai'
import { InternalServerError } from '@peace-net/shared/core/error'
import { categoryScoresSchema } from '@peace-net/shared/schemas/guardian'
import type { CategoryScores } from '@peace-net/shared/types/guardian'
import { generateObject } from 'ai'
import { z } from 'zod'

const systemPrompt = `
# 役割
あなたは画像コンテンツモデレーションの専門家です。
与えられた画像を分析し、不適切な内容のカテゴリーとその深刻度を0から1の範囲で評価してください。

## 注意点
以下に特に注意してください
1. 画像内の細かいディテールや暗示的な表現にも注意してください。隠された意味や背景の文脈を考慮に入れてください。
2. 文化的な文脈や社会的背景を考慮し、日本社会における受け止められ方を反映してください。
3. 画像の内容が年齢や社会的立場によって異なる解釈を引き起こす場合、それも考慮してください。

# 評価方法
各カテゴリーのスコアは以下のガイドラインに従って評価してください。
0.0: 問題なし
0.1-0.2: 軽度の懸念あり
0.3-0.5: 受け手によっては不快または問題があると感じられる可能性あり
0.6-0.8: 中程度の有害性があり、問題視される可能性が高い
0.9-1.0: 深刻な有害性があり、強い懸念や行動が必要な状態

カテゴリーごとのスコアを計算し、'category_scores'オブジェクトに格納してください。
`
// 与えられたテキストを英語で考えて、日本語で回答してください。

/**
* テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するサービスのインターフェース
*
* このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。
* 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。
*
* @interface IGuardianImageService
* @method guardianImage - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @param image - 分析対象の画像(Base64形式)
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export interface IGuardianImageService {
guardianImage(image: string): Promise<CategoryScores>
}

/**
* テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するサービスの実装
*
* このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。
* 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。
*
* @class GuardianService
* @implements IGuardianService
* @param openai - OpenAIProviderのインスタンス
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export class GuardianImageService implements IGuardianImageService {
constructor(private openai: OpenAIProvider) {}

async guardianImage(image: string): Promise<CategoryScores> {
try {
const { object } = await generateObject({
model: this.openai('gpt-4o-mini'),
schema: z.object({ category_scores: categoryScoresSchema }),
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: [{ type: 'image', image }],
},
],
})
return object.category_scores
} catch (error) {
console.error(error)
throw new InternalServerError('Failed to analyze image')
}
}
}
71 changes: 71 additions & 0 deletions apps/api/src/features/guardians/guardianImage.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
GuardianImageInput,
GuardianResult,
} from '@peace-net/shared/types/guardian'
import { failure, success, type Result } from '@peace-net/shared/utils/result'

import {
checkFlagged,
createCategories,
} from '~/features/guardians/guardian.utils'
import { IGuardianImageService } from '~/features/guardians/guardianImage.service'
import { IUsageFacade } from '~/features/usages/usage.facade'

/**
* テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するユースケースのインターフェース
*
* このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。
* また、使用回数をカウントします。
*
* @interface IGuardianImageUseCase
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @param dto - 分析対象のテキストとスコア閾値
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export interface IGuardianImageUseCase {
guardianImage(input: GuardianImageInput): Promise<Result<GuardianResult>>
}

/**
* テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するユースケース
*
* このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。
* また、使用回数をカウントします。
*
* @class GuardianImageUseCase
* @implements IGuardianImageUseCase
* @param guardianImageService - GuardianServiceのインスタンス
* @method guardianImage - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export class GuardianImageUseCase implements IGuardianImageUseCase {
constructor(
private guardianImageService: IGuardianImageService,
private usageFacade: IUsageFacade,
) {}

async guardianImage(
input: GuardianImageInput,
): Promise<Result<GuardianResult>> {
try {
const { image, score_threshold = 0.5, userId, apiKeyId } = input

const categoryScores =
await this.guardianImageService.guardianImage(image)

const flagged = checkFlagged(categoryScores, score_threshold)

const categories = createCategories(categoryScores, score_threshold)

await this.usageFacade.incrementUsage(userId, apiKeyId, 'guardians')

return success({
flagged,
categories,
category_scores: categoryScores,
})
} catch (error) {
return failure(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@ import { NotImplementedError } from '@peace-net/shared/core/error'
import { GuardianTextDTO } from '@peace-net/shared/types/guardian'
import { Context } from 'hono'

import { IGuardianUseCase } from '~/features/guardians/guardian.usecase'
import { IGuardianTextUseCase } from '~/features/guardians/guardianText.usecase'

/**
* テキストの不適切な内容を分析し、カテゴリー別のスコアを提供するコントローラー
*
* このコントローラーは、GuardianUseCaseを使用してテキストの不適切な内容を分析し、結果をクライアントに返します。
*
* @class GuardianController
* @class GuardianTextController
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @method guardianImage - 指定された画像を分析し、不適切な内容の有無を判定します
* @param guardianUseCase - GuardianUseCaseのインスタンス
* @param guardianTextUseCase - GuardianUseCaseのインスタンス
* @returns 分析結果を含むJSONレスポンス
* @throws エラーが発生した場合はエラーレスポンスを返します
*/
export class GuardianController {
constructor(private guardianUseCase: IGuardianUseCase) {}
export class GuardianTextController {
constructor(private guardianUseCase: IGuardianTextUseCase) {}

async guardianText(c: Context) {
const dto = (await c.req.json()) as GuardianTextDTO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ const systemPrompt = `
* このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。
* 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。
*
* @interface IGuardianService
* @interface IGuardianTextService
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @param text - 分析対象のテキスト
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export interface IGuardianService {
export interface IGuardianTextService {
guardianText(text: string, selectedModel: Models): Promise<CategoryScores>
}

Expand All @@ -70,13 +70,13 @@ export interface IGuardianService {
* このサービスは、OpenAI APIを使用して日本語テキストのコンテンツモデレーションを行います。
* 与えられたテキストを分析し、様々なカテゴリーにおける不適切さのスコアを0から1の範囲で評価します。
*
* @class GuardianService
* @implements IGuardianService
* @class GuardianTextService
* @implements IGuardianTextService
* @param openai - OpenAIProviderのインスタンス
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export class GuardianService implements IGuardianService {
export class GuardianTextService implements IGuardianTextService {
constructor(
private openai: OpenAIProvider,
private anthropic: AnthropicProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {
} from '@peace-net/shared/types/guardian'
import { failure, success, type Result } from '@peace-net/shared/utils/result'

import { IGuardianService } from '~/features/guardians/guardian.service'
import {
checkFlagged,
createCategories,
} from '~/features/guardians/guardian.utils'
import { IGuardianTextService } from '~/features/guardians/guardianText.service'
import { IUsageFacade } from '~/features/usages/usage.facade'

/**
Expand All @@ -17,12 +17,12 @@ import { IUsageFacade } from '~/features/usages/usage.facade'
* このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。
* また、使用回数をカウントします。
*
* @interface IGuardianUseCase
* @interface IGuardianTextUseCase
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @param dto - 分析対象のテキストとスコア閾値
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export interface IGuardianUseCase {
export interface IGuardianTextUseCase {
guardianText(input: GuardianTextInput): Promise<Result<GuardianResult>>
}

Expand All @@ -32,15 +32,15 @@ export interface IGuardianUseCase {
* このユースケースは、GuardianServiceを使用してテキストの不適切な内容を分析し、指定されたスコア閾値を超えるかどうかを判定します。
* また、使用回数をカウントします。
*
* @class GuardianUseCase
* @implements IGuardianUseCase
* @class GuardianTextUseCase
* @implements IGuardianTextUseCase
* @param guardianService - GuardianServiceのインスタンス
* @method guardianText - 指定されたテキストを分析し、不適切な内容の有無を判定します
* @returns 分析結果を含むResultオブジェクト。成功時はGuardianResultを、失敗時はエラーを含みます。
*/
export class GuardianUseCase implements IGuardianUseCase {
export class GuardianTextUseCase implements IGuardianTextUseCase {
constructor(
private guardianService: IGuardianService,
private guardianTextService: IGuardianTextService,
private usageFacade: IUsageFacade,
) {}

Expand All @@ -56,7 +56,7 @@ export class GuardianUseCase implements IGuardianUseCase {
apiKeyId,
} = input

const categoryScores = await this.guardianService.guardianText(
const categoryScores = await this.guardianTextService.guardianText(
text,
model,
)
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/schemas/guardian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const guardianTextRequestSchema = z.object({
})

export const guardianImageRequestSchema = z.object({
image_url: z.string(),
image: z.string(),
score_threshold: z.number().max(1).min(0).optional().default(0.5),
})

Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/types/guardian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ export type GuardianTextInput = GuardianTextDTO & {

export type GuardianImageDTO = z.infer<typeof guardianImageRequestSchema>

export type GuardianImageInput = GuardianImageDTO & {
userId: string
apiKeyId: string
}

export type GuardianResult = z.infer<typeof guardianResultSchema>
Loading