| name | add-infrastructure-adapter |
| description | 新しい外部サービス接続を追加する際に使用する。LLMプロバイダ、キャッシュバックエンド、外部APIなどのinfrastructure層実装の追加手順に従う。 |
インフラストラクチャ層の拡張
外部サービスとの接続を追加する際のパターンを示す。既存の実装として、LLMプロバイダ(gemini/noop)、キャッシュ(SQLite/メモリ)、Misskey API、RSS取得がある。
新しいLLMプロバイダの追加
LLMプロバイダを追加する場合、ファクトリパターンに従う。
事前に次を確定する。
- プロバイダ名(例: bedrock)
- 必須設定項目(APIキー、モデル名、リージョンなど)
- 失敗時の扱い(初期化失敗時とnoopフォールバック失敗時の両方)
プロバイダ名はLLM_PROVIDERに設定する小文字値を使う(例: gemini, bedrock)。
上記3点が未確定なら実装を開始しない。特に必須設定項目は「変数名」「必須/任意」「default値」を表で先に決める。
実装開始ゲート:
プロバイダ名
必須/任意/デフォルト表
失敗時の扱い
この3点がそろうまでコード変更しない。
このプロジェクトでは分岐キーにLLM_PROVIDERを使う。値はcfg.GetLLMConfig().Providerを経由してllm.Config.Providerに渡され、internal/infrastructure/llm/summarizer.goのswitch cfg.Providerで選択される。
このプロジェクトではLLM関連設定をinternal/interfaces/config/config.goで一元管理する。LLM_REGIONのような項目をconfigに置くことは本プロジェクトの規約上許容する。
このSkillの対象は「単一の有効プロバイダ選択」のみ。複数プロバイダ同時初期化や実行時切り替えは対象外。
- internal/infrastructure/llm/に{プロバイダ名}_summarizer.goを作成する
- repository.SummarizerRepositoryインターフェースを実装する
実装するメソッドシグネチャ:
Summarize(ctx context.Context, url, title string) (string, error)
IsEnabled() bool
- internal/infrastructure/llm/summarizer.goのNewSummarizerRepository内のswitch文にcaseを追加する
case追加の最小例:
switch cfg.Provider {
case "gemini":
return newGeminiSummarizer(ctx, cfg)
case "bedrock":
return newBedrockSummarizer(ctx, cfg)
case "myprovider":
return newMyproviderSummarizer(ctx, cfg)
case "noop", "":
return newNoopSummarizer(), nil
default:
return nil, fmt.Errorf("unknown LLM provider: %s", cfg.Provider)
}
- internal/interfaces/config/config.goに必要な設定を追加する
- main.goでinternal/interfaces/configの値をllm.Configへ変換して渡す
llm.Configの対象フィールド:
Provider string
APIKey string
Model string
Region string
MaxTokens int
SystemInstruction string
Timeout time.Duration
llm.Configはinternal/infrastructure/llm/summarizer.goに既存定義があるため、新規に重複定義しない。
環境変数名は既存規約のLLM_*を使う。
config.goの追加例:
LLMProvider string `envconfig:"LLM_PROVIDER" default:""`
LLMAPIKey string `envconfig:"LLM_API_KEY"`
LLMModel string `envconfig:"LLM_MODEL"`
LLMRegion string `envconfig:"LLM_REGION" default:""`
LLMTimeout int `envconfig:"LLM_TIMEOUT" default:"30"`
必須/任意の判定手順:
- プロバイダSDK/API仕様で必須入力を列挙する
- 列挙した必須入力を
requiredとしてconfig.goに追加する
- 任意入力のみdefault値を設定する
GetLLMConfig()に全項目を追加する
- アダプタコンストラクタでrequired欠落を検証してエラー返却する
エラーはfmt.Errorf("...: %w", err)でラップして返す。
required判定はenvconfigのrequiredタグではなく、プロバイダごとにコンストラクタ内で行う。
required判定の最小例:
if cfg.Model == "" {
return nil, fmt.Errorf("myprovider model is required")
}
main.goでは次の順で配線する。
llmCfg := cfg.GetLLMConfig()
llm.NewSummarizerRepository(ctx, llm.Config{...})にllmCfgの各フィールドを明示的にマッピング
- 初期化失敗時は既存実装と同様に
noopへフォールバック
noopフォールバックも失敗した場合は警告を出力し、要約機能なしで継続する
llmCfg.TimeoutはGetLLMConfig()で秒からtime.Durationへ変換済みの値をそのまま渡す。
配線の最小例:
llmCfg := cfg.GetLLMConfig()
summarizerRepo, err := llm.NewSummarizerRepository(ctx, llm.Config{
Provider: llmCfg.Provider,
APIKey: llmCfg.APIKey,
Model: llmCfg.Model,
Region: llmCfg.Region,
MaxTokens: llmCfg.MaxTokens,
Timeout: llmCfg.Timeout,
SystemInstruction: llmCfg.SystemInstruction,
})
if err != nil {
log.Printf("Warning: LLM initialization failed: %v", err)
log.Println("Attempting fallback to noop summarizer...")
summarizerRepo, err = llm.NewSummarizerRepository(ctx, llm.Config{Provider: "noop"})
if err != nil {
log.Printf("Warning: noop summarizer initialization failed: %v", err)
log.Println("Continuing without summarization feature")
}
}
コンストラクタはエクスポートせず先頭小文字で定義する。Configから必要なフィールドを取得し、不足する場合はエラーを返す。タイムアウトはcontext.WithTimeoutで制御する。
context.WithTimeoutはSummarizeメソッド内で適用する。
タイムアウトのデフォルト値は各アダプタのコンストラクタで適用する。既存実装に合わせてcfg.Timeout == 0の場合は30 * time.Secondを設定し、Summarize内で固定値を再定義しない。
最小例:
func (s *myProviderSummarizer) Summarize(ctx context.Context, url, title string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
return "", nil
}
IsEnabled()は有効な要約器でtrueを返し、無効化用実装(noop)でfalseを返す。
IsEnabled()はinternal/application/rss_feed_service.goから参照される。
noopフォールバック失敗時の扱い:
LLMプロバイダ初期化はmain.goで起動時に行われる。第一選択プロバイダが失敗した場合、noopサマライザーへフォールバックする。noopフォールバックも失敗した場合は警告を出力し、要約機能なしで継続する。
llm.Configへのマッピングに追加の一時Config構造体は作らない。
repository.SummarizerRepositoryは既存インターフェースを使い、新規定義しない。
タイムアウト値はinternal/interfaces/config/config.goのLLM_TIMEOUT(default 30秒)をGetLLMConfig()経由で渡す。新しいアダプタ実装では、Summarize内で固定値を新規定義しない。既存プロバイダ実装にあるcfg.Timeout == 0時の30秒フォールバックは互換性維持の防御策として許容する。
既存プロバイダの必須項目例:
- gemini:
LLM_API_KEY, LLM_MODEL
- bedrock:
LLM_MODEL, LLM_REGION
テストは{プロバイダ名}_summarizer_test.goを追加し、最低限次を検証する。
- 必須設定不足時にエラーを返す
- Summarizeの正常系で期待する要約が返る
- 外部APIエラーをラップして返す
- context timeout時に失敗する
テスト時の外部API呼び出しはhttptestまたは差し替え可能なクライアントインターフェースでモックし、実ネットワークに依存しない。
モック方式はhttptestを第一選択とし、SDKの都合で難しい場合のみクライアントインターフェース差し替えを使う。
テストはテーブル駆動で記述する。
新しいキャッシュバックエンドの追加
- internal/infrastructure/storage/に{バックエンド名}_cache.goを作成する
- repository.CacheRepositoryインターフェースを実装する(GetLatestPublishedTime, SaveLatestPublishedTime, IsProcessed, MarkAsProcessedの4メソッド)
- main.goの分岐ロジックに新しいバックエンドの選択肢を追加する
io.Closerの実装が必要な場合はClose() errorメソッドも追加する。CleanupOldGUIDsのようなバックエンド固有のメソッドはmain.goで型アサーションして使用する。
新しい外部APIの追加
- domain/repository/に新しいインターフェースを定義する
- internal/infrastructure/{サービス名}/にパッケージを作成する
- インターフェースを実装する(構造体はエクスポートしない、コンストラクタはNew{型名}でインターフェース型を返す)
- 必要な設定をinternal/interfaces/config/config.goのConfig構造体にenvconfigタグ付きで追加する
- main.goで具象型を生成しサービスに注入する
接続先やAPIキーなどの実装固有の設定は、infrastructure層内にConfig構造体を定義する。LLMプロバイダに限ってはinternal/interfaces/config/config.goで一元管理する。internal/interfaces/config/config.goの設定からinfrastructure層のConfigへの変換はmain.goで行う。
設定の追加に伴う対応
- internal/interfaces/config/config.goにフィールドを追加する
- internal/interfaces/config/config_test.goにテストを追加する
- main.goで新しい設定を読み取り、infrastructure層に渡す
- READMEに環境変数の説明を追記する