参考
api/
└─ app/
├─ main.py # FastAPI起動。各routerをマウント
├─ config.py # 環境変数読み込み(DB/Chroma等)
├─ db.py # DB接続(SQLAlchemy/psycopg2等)
├─ routers/
│ ├─ extract.py # ① 抽出: /extract/view_common, /extract/field
│ ├─ translate.py # ② 翻訳: /translate/run, /translate(GET)
│ ├─ writeback.py # ③ 書き戻し(任意): /writeback/view_common, /writeback/field
│ ├─ package.py # ④ パッケージ: /chroma/package, /chroma/docs(GET)
│ ├─ chroma.py # ⑤ Upsert: /chroma/upsert
│ ├─ status.py # ダッシュボード: /status/summary, /samples/trace
│ └─ __init__.py
├─ schemas/
│ ├─ extract_view_common.py # 入出力DTO(action_xmlids/targets等)
│ ├─ extract_field.py # 入出力DTO(models/fields等)
│ ├─ translate_run.py # 入出力DTO(limit/entities等)
│ ├─ writeback_view_common.py # 入出力DTO(mode等)
│ ├─ writeback_field.py # 入出力DTO
│ ├─ chroma_package.py # 入出力DTO(collections/lang等)
│ ├─ chroma_upsert.py # 入出力DTO(collections/limit等)
│ ├─ status.py # サマリ応答DTO
│ └─ common.py # 共通型: NaturalKey, Entity, Lang, Summary
├─ services/
│ ├─ extract.py # 抽出オーケストレーション(view_common/fieldを切替)
│ ├─ translate.py # 翻訳ワーカー(pending→translated)
│ ├─ writeback.py # 英語書き戻し(任意)
│ ├─ package.py # translated→ready_for_chroma + chroma_doc作成
│ ├─ chroma_client.py # Chroma接続(upsert/query)
│ └─ natural_text.py # doc_text生成(日本語テンプレ整形)
├─ repos/
│ ├─ portal_translate_repo.py # portal_translate CRUD/検索
│ ├─ portal_chroma_doc_repo.py # portal_chroma_doc CRUD/検索
│ ├─ portal_view_common_repo.py # portal_view_common 読取
│ ├─ portal_field_repo.py # portal_fields 読取
│ └─ portal_model_repo.py # portal_model 読取(model_table等補完)
├─ utils/
│ ├─ natural_key.py # natural_key生成/検証(正規化・lower/trim)
│ ├─ lang_detect.py # 日本語判定(ぁ-ん/ァ-ン/一-龥)
│ ├─ html_strip.py # HTML除去(help_*_html→text)
│ ├─ text_hash.py # source_hash計算(差分判定)
│ └─ time.py # ISO時刻などユーティリティ
├─ openapi/
│ └─ openapi.yaml # 手書き/自動出力の固定化(必要なら)
└─ sql/
├─ 000_extensions.sql # pgcrypto/tg_touch等の共通関数
├─ 010_portal_core.sql # portal_model/fields/view_common/view…(既存)
├─ 020_portal_translate.sql # portal_translate DDL
└─ 030_portal_chroma_doc.sql # portal_chroma_doc DDL
routers(HTTP I/F)
- extract.py
/extract/view_common:action_xmlid群→portal_translateにUPSERT(ai_purpose/helpのみ、JAあり&EN未入力を判定)/extract/field:model/field指定→portal_translateにUPSERT(JAラベル/用途)
- translate.py
/translate/run:status=pendingを翻訳しtranslated_*を保存→translatedGET /translate:一覧・検索
- writeback.py(任意)
/writeback/view_common:ai_purpose_i18n.en_US/help_en_textを更新/writeback/field:label_i18n.en_USを更新(方針次第)
- package.py
/chroma/package:translatedを 日本語doc+厳密メタに整形→portal_chroma_docへUPSERT(queued)+ready_for_chromaGET /chroma/docs:ステージング一覧
- chroma.py
/chroma/upsert:portal_chroma_doc.status='queued'をChromaへ投入→upserted
- status.py
/status/summary:件数サマリ/samples/trace:natural_key単位の通し状況
services(ドメインロジック)
- extract.py:テーブル読み→日本語判定→natural_key生成→translate UPSERT(mode: upsert/skip/if_changed)
- translate.py:翻訳方向/数の制御、失敗リトライ、
source_hashで二度訳抑止 - writeback.py:英語列の更新(ポリシー:overwrite/skip_if_exists)
- package.py:テンプレ(
natural_text.py)で日本語doc生成、必須メタ収集、portal_chroma_docUPSERT - chroma_client.py:コレクション作成/Upsert/ヘルスチェック
repos(DBアクセス)
portal_translate_repo.py:pending/translatedの取得・UPSERTportal_chroma_doc_repo.py:queuedの取得・UPSERT- そのほか
view_common/field/modelリード用
utils(共通関数)
- natural_key.py:
view_common::<xmlid>::help等の規約化 - lang_detect.py:
[ぁ-んァ-ン一-龥]判定(高速) - html_strip.py:
help_i18n_html→help_ja_text生成時の保険 - text_hash.py:
label+purpose+payloadからsource_hash生成(冪等化)
sql(DDL固定化)
- 020_portal_translate.sql / 030_portal_chroma_doc.sql:すでにお渡ししたDDLを入れる場所
エンドポイント ↔ ファイル対応早見表
| エンドポイント | router | 主サービス | 主テーブル |
|---|---|---|---|
POST /extract/view_common | routers/extract.py | services/extract.py | portal_view_common → portal_translate |
POST /extract/field | routers/extract.py | services/extract.py | portal_fields → portal_translate |
POST /translate/run | routers/translate.py | services/translate.py | portal_translate |
POST /writeback/view_common | routers/writeback.py | services/writeback.py | portal_view_common |
POST /writeback/field | routers/writeback.py | services/writeback.py | portal_fields |
POST /chroma/package | routers/package.py | services/package.py | portal_translate → portal_chroma_doc |
POST /chroma/upsert | routers/chroma.py | services/chroma_client.py | portal_chroma_doc → Chroma |
GET /status/summary | routers/status.py | (複合) | translate/chroma_doc 集計 |
GET /samples/trace | routers/status.py | (複合) | natural_keyトレース |
1) ルータ構成(ファイル)
api/app/routers/
portal_model.py # /portal/model …
portal_field.py # /portal/field …
portal_view_common.py # /portal/view_common …
portal_view.py # /portal/view …
portal_tab.py # /portal/tab …
portal_smart_button.py # /portal/smart_button …
portal_menu.py # /portal/menu …
それぞれの内部では、DB操作は
repos/portal_*.py、Odoo標準の参照はrepos/ir_*_src_repo.pyを呼ぶ想定です。
翻訳キュー投入(translate)は既存の/extract/*に任せる運用でも、各 portal ルータから“キュー投入”の補助アクションを提供してもOKです。
2) エンドポイント一覧(役割別・入出力の概要)
■ モデル:/portal/model
- GET
/portal/model
一覧(q,model,limit,cursorなど) - GET
/portal/model/{id}
詳細取得 - POST
/portal/model
新規作成(model,model_table,label_i18n,notes) - PATCH
/portal/model/{id}
部分更新 - DELETE
/portal/model/{id}(運用方針で制限可) - POST
/portal/model/import
Odooミラー(ir_model_src)から選択取込。
入力:{ "models": ["sale.order", ...], "scaffold": true }
出力:{ created, updated, skipped }scaffold=trueなら初期portal_viewやportal_fieldsの骨組み生成を同時に実施。
■ フィールド:/portal/field
- GET
/portal/field
一覧(model,field_name,origin,limit…) - GET
/portal/field/{id}
詳細 - POST
/portal/field
新規作成(個別) - PATCH
/portal/field/{id}
更新(label_i18n,notes,ttypeなど) - DELETE
/portal/field/{id} - POST
/portal/field/importir_field_srcからモデル単位/フィールド単位で選択取込。
入力:{ "model": "sale.order", "fields": ["partner_id","amount_total"] }
出力:{ created, updated, skipped } - POST
/portal/field/extract(任意)
指定のモデル/フィールドを translate にUPSERT(翻訳キュー投入)。
■ ビュー共通(アクション起点):/portal/view_common
- GET
/portal/view_common
一覧(action_xmlid,model,primary_view_typeなど) - GET
/portal/view_common/{id}
詳細 - POST
/portal/view_common
手動作成・更新用(あまり使わない想定) - PATCH
/portal/view_common/{id} - DELETE
/portal/view_common/{id} - POST
/portal/view_common/importir_view_src (action-centric)からaction_xmlid選択取込(UPSERT)。
入力:{ "action_xmlids": ["sale.action_quotations", ...] }
出力:{ created, updated, skipped } - POST
/portal/view_common/bootstrap_view
取込済みview_types[]を展開し、対応するportal_viewの骨組みを一括作成。
入力:{ "action_xmlids": [...], "set_primary_from_common": true } - POST
/portal/view_common/extract(任意)
指定 action についてai_purpose/help_ja_textを translate にUPSERT。
入力:{ "action_xmlids": [...], "targets": ["ai_purpose","help"] }
■ ビュー(種別ごとの設定):/portal/view
- GET
/portal/view
一覧(common_id,view_type,model,is_primary…) - GET
/portal/view/{id}
詳細 - POST
/portal/view
手動作成(common_id,view_type, 付随設定) - PATCH
/portal/view/{id}
種別ごとの設定を更新(list/form/calendar…) - POST
/portal/view/set_primary
1つのcommon_id内で 主ビューを設定(トリガで単一化)。
入力:{ "common_id": 123, "view_type": "list" } - POST
/portal/view/bootstrap_from_commonview_common.bootstrap_viewと同義(呼び分けたい場合に)
■ タブ:/portal/tab
- GET
/portal/tab
一覧(view_id,tab_keyなど) - GET
/portal/tab/{id} - POST
/portal/tab
作成(view_id,tab_key,tab_label_ja/en, オプション群) - PATCH
/portal/tab/{id} - DELETE
/portal/tab/{id} - POST
/portal/tab/bulk_upsert
複数タブを一括登録。
入力:[{ "view_id": 1, "tab_key": "main", "tab_label_ja": "基本", ...}, ...]
■ スマートボタン:/portal/smart_button
- GET
/portal/smart_button
一覧(view_id,button_key,action_type…) - GET
/portal/smart_button/{id} - POST
/portal/smart_button
作成(view_id,button_key,label_i18n,action_type等) - PATCH
/portal/smart_button/{id} - DELETE
/portal/smart_button/{id} - POST
/portal/smart_button/bulk_upsert
複数ボタン一括登録
■ メニュー:/portal/menu
ir_menu_srcをミラーしていない前提なら、まずは手動管理+将来の取込口を予約。
- GET
/portal/menu/ GET/portal/menu/{id} - POST
/portal/menu/ PATCH/portal/menu/{id}/ DELETE - POST
/portal/menu/bulk_upsert - POST
/portal/menu/import(将来)ir_menu_srcを用意したら action_xmlid/parent をキーに取込。
3) 既存のパイプラインAPIとの関係
portal_*ルータは ポータル本体の状態を作る/保つエンドポイント群です(CRUD・標準取込・ブートストラップ)。- 翻訳・Chroma系は、既案の以下で運用:
/extract/*… translate にキュー投入(各 portal_* からでも実行可)/translate/run… 翻訳/chroma/package… 日本語doc+厳密メタのパッケージング/chroma/upsert… ベクトル投入
つまり、まず
/portal/*でポータルの土台を作る → 必要なところだけ/extract/*に渡す、が日常の流れです。
4) 典型フロー(例)
- アクションを選んで取り込む:
POST /portal/view_common/import(action_xmlid群) - 対応するビュー行を生成:
POST /portal/view_common/bootstrap_view - 必要なフィールドを取り込む:
POST /portal/field/import(model+fields) - 翻訳キューに積む(必要な時だけ):
POST /portal/view_common/extract、POST /portal/field/extract - 翻訳→Chroma:
/translate/run→/chroma/package→/chroma/upsert
フェーズ0 まずOdooにアクセス。pgadiminで表示
フェーズA:骨格固定(1〜2スプリントの核)
目的:後戻りが大きい部分を最初に確定し、以降は実装を足すだけにする
- API仕様の凍結
- 成果物:OpenAPI 3.1(既に提示のYAML)を
/openapi/openapi.yamlとして確定 - DoD:
/docsで表示可・基本パスが全て列挙されている(モックOK)
- 成果物:OpenAPI 3.1(既に提示のYAML)を
- DDLの確定・マイグレーション基盤
- 成果物:
sql/000_extensions.sql、010_portal_core.sql、020_portal_translate.sql、030_portal_chroma_doc.sql - 変更点反映:
portal_viewのcalendar_*_fieldを text に変更済み - DoD:ローカル/CIで“初期化→再適用”が冪等
- 成果物:
- プロジェクト骨格+ヘルスエンドポイント
- 成果物:
/app/main.py(healthz/livez/startupz)、/app/config.py、/app/db.py、/app/repos/*雛形 - DoD:
GET /healthz = 200、ログと設定の読み込みが通る
- 成果物:
フェーズB:Repo層の完成(DB I/Oの中核)
目的:全てのルータがここを呼べば済む状態にする
- Portal Repos(書込あり)
portal_model_repo.py/portal_field_repo.py/portal_view_common_repo.py/portal_view_repo.py- 共通:UPSERT/ページング/自然キー生成、
updated_atの自動タッチ - DoD:ユニットテストでCRUDと一意制約が通る
- IRソース Repos(読み取り専用)
ir_model_src_repo.py/ir_field_src_repo.py/ir_view_src_repo.py(action-centric)- DoD:
SELECT系が想定件数を返す(当初はスタブ可)
フェーズC:Odooミラー(action-centric)を“参照可能”にする
目的:本番に近いデータでPortalの取込を試せる状態
- 成果物:あなたが提示したSQLで
public.ir_*_srcを再構築するスクリプト(ローカル&Cron両対応) - DoD:
ir_model_src / ir_field_src / ir_view_srcの件数が取得できる
※K8s用のCronJobは後段。ここでは実行SQLとRepoの動作確認のみ
フェーズD:Portalの“取込API”から先に実装
目的:ポータルの背骨を最速で成立させる
- /portal/view_common/import(action_xmlid 選択UPSERT)
- /portal/view_common/bootstrap_view(
view_types[]→portal_view骨組み作成) - /portal/field/import(モデル/フィールド選択UPSERT)
4.(必要に応じて)/portal/model/import
- DoD:手動呼び出しで
portal_view_common / portal_view / portal_fieldsが一意UPSERTされる
フェーズE:抽出(translate投入)
目的:翻訳対象を translate に冪等UPSERT
- translateテーブルの状態遷移/自然キー/ソースハッシュの確定(仕様は保持)
- /extract/view_common(
ai_purpose/help_ja_text:JAあり & EN空のみ) - /extract/field(label/notesのJAから)
- DoD:同じリクエストを繰り返しても
pendingが重複せずに集約される
フェーズF:翻訳(ジョブ)
目的:pending → translated を安定運転
- 翻訳アダプタ(プロバイダ差替え可能)
- /translate/run(limit/対象entity/エラー時failed)
- ルール:同一source_hashは再翻訳しない
- DoD:正常・失敗・再実行の各ケースがE2Eで確認できる
フェーズG:パッケージング(日本語doc+厳密メタ)
目的:Chromaに投げられる完成形を作る
- /chroma/package:
translatedのみ→portal_chroma_docにUPSERT(queued) - docテンプレ(日本語):Field/Viewのフォーマット・メタ詰め(model, model_table, field_name or action_xmlid など)
- DoD:
doc_id = natural_key::langで一意、同一source_hashはスキップされる
フェーズH:Chroma upsert
目的:検索に載せる
- Chroma Client(接続/collection作成/embeddings)
- /chroma/upsert(
queuedのみ対象、dry_runあり)
- DoD:
upsertedが期待件数に増分、簡単な日本語クエリでヒット
フェーズI:書き戻し(任意)&ステータス
目的:UIとの整合・運用の見える化
- /writeback/view_common・/writeback/field(デフォルトは
skip_if_exists) - /status/summary, /samples/trace
- DoD:自然キー単位で抽出→翻訳→パッケージ→upsertの通し状況が見える
フェーズJ:Kubernetes向けの“開発最適化”実装
目的:K8sで開発/検証が回しやすい足回り
- ヘルスチェック(readiness/liveness/startup)を本実装に
- Bearer認証/監査ログ/リクエストIDミドルウェア
- Idempotency-Key(PUT/POSTの多重防止:後方互換で追加)
- Config分離(dev/prodでConfigMap/Secret差替えが効くこと)
- DoD:K8s上でのローリング更新/再実行で“二重実行・ダウン”が起きない
フェーズK:Cron層(K8s化)
目的:人手なしで回る
- 既存のCronJob雛形に合わせ、APIコール型で
extract → translate → package → upsertを接続 - DoD:Cronの連鎖で
queued → upsertedが自然に進む(失敗時の再試行ポリシー確認)
依存関係まとめ(最短径)
- D → E → F → G → H が“検索までの最短ライン”
- C(Odooミラー)はDの直前までに参照可能にしておけばOK
- I/J/K は品質・運用の層。最短ラインが通った後に入れる
テストの当て方(各フェーズのDoDに直結)
- ユニット:RepoのCRUD/UPSERT/自然キー/ハッシュ
- API結合:各エンドポイントの冪等性とバリデーション
- E2E(最短ライン):
view_common.import→bootstrap_view→field.importextract.*→translate/run→chroma/package→chroma/upsert- 日本語クエリでヒット(サンプル固定)
先に作るべき“最小セット”(実装順の目印)
- DDL & Repo(Portal/IR)
- view_common.import / bootstrap_view / field.import
- extract(view_common/field)
- translate/run
- chroma/package
- chroma/upsert
(以降:writeback → status → k8s最適化/cron)
この順で進めると、“Odooを主に使うユーザー”視点で、最低限の取込と検索が先に成立し、その後に品質と運用性を段階的に積み増せます。
コメントを残す