/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } フロントバックエンド融合 全体ゴール(あなた側) – Raqqa

フロントバックエンド融合 全体ゴール(あなた側)

オフショアに渡す前に、あなた側でここまでできていれば十分、というラインをこう置きます:

  • nlq-dev バックエンドのエンドポイント構成とDBスキーマがほぼ固定されている
  • k8s/base(nlq-api, Chroma, Neo4jスケルトン)が用意されていて、
    ローカルで kubectl apply すれば最低限動く
  • nlq-dev ⇔ dev-portal(Postgres / Chroma)のインタフェース仕様が文書化されている
  • Neo4j は k8sとenvだけ用意した「席だけある」状態

これを、無理のない粒度でタスクに分けます。


タスク1:現状整理&リポジトリの基本ルール固め

目的: あとからオフショアに「この形を守ってね」と言えるようにする。

  • nlq-dev リポジトリのトップに README.md を置く(簡易でOK)
    • プロジェクト概要(NLQ → SQL → 分析)
    • ディレクトリ構成(api/, k8s/, sql/ など)
    • ローカル起動方法(今できる範囲だけ)
  • .env.example を整備
    • DATABASE_URL
    • CHROMA_URL
    • NEO4J_URI(空でいいがキーだけ書いておく)
    • OPENAI_API_KEY など
  • バックエンドのエントリポイントを一箇所に固定
    • 例: api/app/main.py(portal-dev と似た構造に寄せる)

👉 ここは「1日あれば終わる」レベルの軽作業です。


タスク2:nlq-dev バックエンドのエンドポイント骨格を確定

目的: フロントとオフショアに「この API 前提で作って」と渡せる状態にする。

  • NLQ関連の主要エンドポイントだけは固定しておく
    • 例:
      • POST /nlq/plan
      • POST /nlq/execute
      • GET /nlq/history
      • POST /nlq/analysis
      • POST /nlq/analysis/{analysis_id}/upsert
  • Pydanticスキーマを軽く整理
    • PlanRequest, ExecuteRequest, AnalysisResponse, DiffUpsertResponse など
  • 中身のロジックはダミーでもOKだが「I/Fだけは変えない」方針にする
    • 例:/nlq/analysis は今はスタブで "This is dummy analysis" を返すだけでもよい

👉 ここができると、フロント実装はかなり任せやすくなります


タスク3:Chroma 設計&環境変数の固定

目的: Dev-Portal と nlq-dev が 同じ前提で Chroma を使えるようにする。

  • Chromaのコレクション命名ルールを決めておく
    • 例:
      • Dev-Portal フィールドJA: portal_field_ja
      • Dev-Portal ビューJA: portal_view_common_ja
      • NLQセッション用: nlq_sessions_ja / nlq_sessions_en
  • config.py 的な場所で環境変数→設定のマッピングを書く
    • CHROMA_URL
    • CHROMA_COLLECTION_FIELD_JA
    • CHROMA_COLLECTION_VIEW_JA
    • CHROMA_DEFAULT_COLLECTION(あれば)
  • /nlq/analysis/{analysis_id}/upsert で使うコレクションのルールを一言コメントに残す
    • 例:「NLQの分析コメントは nlq_sessions_{lang} に入れる」

👉 これは「後から変更したくない設計」の部分なので、今のうちに軽くでも決めておくのが吉です。


タスク4:k8s/base(ローカル用)の骨格を作る

目的: 後でオフショアが overlays/dev/prod を増築しやすい「基礎」を作る。

最小構成はこれくらいでOKです:

  • k8s/base/nlq-api/deployment.yaml
    • app: nlq-api
    • containerPort: 8000
    • readinessProbe: /health/ready
  • k8s/base/nlq-api/service.yaml
    • ClusterIP でOK。
  • k8s/base/chroma/deployment.yamlservice.yaml
    • 既に portal-dev で使っている構成をほぼコピペでOK(namespace だけ注意)
  • k8s/base/neo4j/deployment(or statefulset).yamlservice.yaml
    • 今は 中身はほぼデフォルト & 1ノード でOK
  • k8s/base/postgres
    • 既存の Odoo/Dev-Portalと同じ Postgres を使うなら、ここでは触らず「外部リソース」と見なしてもよい

👉 この段階では overlays/dev/prod は空でもよくて、base が揃えばOK
(後で「ここをオフショアに触らせる」前提)


タスク5:nlq-dev ⇔ Dev-Portal のインタフェース仕様を書面化

目的: コードをいじる前に、どの情報を dev-portal 側からもらうかを明文化。

  • Markdown 1ファイル(例:docs/nlq-dev-portal-integration.md)を書く
    • nlq-dev が必要とするメタ情報:
      • モデル名/テーブル名
      • 各フィールドのラベル・説明
      • Viewの説明/用途(あれば)
    • それをどこから取るか:
      • 直接 Postgres(portal_model, portal_field, portal_view_*)を読むのか
      • それとも Dev-Portal API を叩くのか
    • 参照方向:
      • きほん「nlq-dev → Dev-PortalのDB/Chromaを読むだけ」(書き込みはしない)と決める、など
  • ここに「後で絶対に変えたくないルール」も一言書いておく
    • 例:model + field_name は自然キーとして扱う、など

👉 これがあると、**Dev-PortalのUIやオフショアの実装を変えるときの“軸”**になります。


タスク6:Neo4j スケルトンだけ先に用意(中身ナシでOK)

目的: 3ヶ月後にいつでも Odoo→Neo4j を始められる“席”を用意する。

  • k8s/base/neo4j を作成(上のタスク4と一体でやってOK)
  • .env.exampleNEO4J_URI, NEO4J_USER, NEO4J_PASSWORD を追記
  • api/app/services/graph_client.py みたいなファイルを1つだけ作る
    • 中身は「接続クライアントの雛形」だけでOK:
      • 接続を作る関数
      • ping() 的なヘルスチェックだけ実装
  • README に「Neo4j は今は“空の席”。将来 Odoo データ連携を入れる予定」と一文書いておく

👉 これで、「今は使ってないけど、ちゃんと設計に組み込まれている」状態になります。


タスク7:オフショアへ渡すための “まとめファイル” を作る

目的: ここまでできたら一旦「私のターン終了」、と区切れるようにする。

  • docs/offshore-hand-off.md みたいなファイルを作る
    • プロジェクト全体像(nlq-dev + dev-portal + Chroma + Neo4j + Odoo)
    • 今回、オフショアにお願いしたい領域:
      • フロント実装(nlq-dev, Dev-Portal)
      • k8s/overlays/dev, overlays/prod
      • AWS(EKS/ALB/ACM) 移行
    • 「ここは触らないでほしい」という領域:
      • DBスキーマ
      • Chromaコレクション設計
      • Neo4jのCypher(当面)
    • ざっくり PR 粒度の例(NLQ-PR-01 など)

👉 この1ファイルがあれば、「そろそろハンドオフしよう」と思ったときにすぐ動けます。


進め方のおすすめ

  • まずは タスク1 → 2 → 3(設計とAPI周り)を先にやる
  • そのあと タスク4(k8s/base) でローカルの“箱”を作る
  • 時間と気力があれば タスク5/6/7 を少しずつ埋めていく

どれも「丸一日〜数日で終わるサイズ」にしてあるので、
仕事の合間にちょこちょこ進めつつ、頃合いを見てオフショアに投げる、というスタイルが取りやすいと思います。


「タスク2の API 骨格だけもう少し具体的に見たい」みたいな感じなら、
次は nlq-dev 用の openapi スケッチか routers/nlq.py のテンプレを書き起こすところから一緒にやりましょうか。

あなた:

この後のオフショアのタスクを同じように分割して提示してください。その際、理解しておくべきことも書いて欲しいです。

ChatGPT:

思考時間: 1m 12s

いいですね、「まずこちらで土台を作ってから、まとまった塊をオフショアに渡す」やり方は絶対やりやすいです 👍
では、その**“土台ができた後” にオフショアにお願いするタスク**を、こちら側のタスクと同じノリで分割してみます。


前提(あなた側でやるところ)

ここまでが終わっている前提で書きます:

  • nlq-dev の バックエンドAPIの骨格とスキーマがほぼ固定
  • k8s/base/* が一通りある(nlq-api, chroma, neo4j の Deployment/Service など)
  • Chroma のコレクション名・メタデータの方針が決まっている
  • docs/offshore-hand-off.mddocs/nlq-dev-portal-integration.md
    **「触っていいところ/触ってほしくないところ」**が文章化されている

この状態からスタート、という想定です。


Offshore-Task 1:キックオフ&環境再現

目的
オフショア側が「全体像」と「守備範囲」を理解し、ローカル or dev クラスタで nlq-dev を動かせる状態にする。

やってもらうこと

  • nlq-dev リポジトリを clone
  • README.md / docs/offshore-hand-off.md / docs/nlq-dev-portal-integration.md を一通り読む
  • あなたが用意した .env.example をもとにローカル用 .env を作成
  • あなたの手順書どおりに
    • ローカル or dev クラスタに nlq-api, Chroma, Neo4j(骨格)を起動
    • curlhttpie, VSCode REST などで /health/ready や NLQ API を軽く叩いてみる

★理解しておくべきこと

  • プロジェクト全体構成:
    • Odoo(業務DB)
    • Dev-Portal(メタ情報+Chroma)
    • nlq-dev(NLQ→SQL→分析+差分 upsert)
  • DBスキーマとChroma/Neo4j設計は Raqqa側がオーナー」であり、
    • オフショアは「それを safely 利用する側」であること
  • k8s/basek8s/overlays/* の役割の違い
    • base = あなたが決めた“標準形”
    • overlays = 環境ごとの調整(ここはオフショアの主戦場)

Offshore-Task 2:nlq-dev フロント実装(NLQフロー基本画面)

目的
NLQ の「計画 → 実行 → 履歴」を、あなたが用意した API 骨格に沿ってフロントで操作できるようにする。

やってもらうこと

  • Figma / 画面仕様にあわせて、NLQ の基本画面を実装
    • NLQ入力画面(テキストボックス+送信ボタン)
    • プラン表示画面(どんな SQL が出るか)
    • 実行結果画面(テーブル表示、件数)
    • 履歴一覧(過去の NLQ セッション一覧 or タイムライン)
  • あなたが確定した API を利用して接続
    • POST /nlq/plan
    • POST /nlq/execute
    • GET /nlq/history
  • 最低限のエラーハンドリングを実装
    • HTTPエラー / タイムアウト時のメッセージ表示
    • ローディングのスピナーなど

★理解しておくべきこと

  • NLQ の基本フロー
    • 自然言語 → plan(候補SQL) → execute(Postgres)→ 結果 → history
  • APIはあなたが決めた契約が絶対であり、
    「フロントに合わせてAPIを変える」のではなく「わからない点は質問する」方針であること
  • Odoo のテーブル構造までは深く理解していなくてもよいが、
    • SELECTのみ実行
    • LIMITが必ずかかる
      など、安全系の思想は頭に入れておくこと

Offshore-Task 3:nlq-dev 分析+差分アップサート UI & 配線

目的
NLQ結果に対してAI分析を行い、その分析結果を Chroma に diff-doc として upsert する一連の流れをフロントから操作できるようにする。

やってもらうこと

  • Figma で定義済みの「分析画面」「タイムライン」を実装
    • 対象ステップに対する「AI分析結果」の表示
      -「次のアクション」「要約」などのセクション
  • 分析APIとの連携
    • POST /nlq/analysis で分析を生成(もしくは取得)
    • 分析ステータスの表示(処理中/完了/失敗)
  • Diff upsert 操作の実装
    • POST /nlq/analysis/{analysis_id}/upsert を呼び出すボタン
    • 成功時/失敗時のトースト表示
  • ログ確認手順をREADME等にまとめる
    • どのログを見れば doc_id や collection がわかるか

※ コアなプロンプト設計や、メタデータの意味付けはあなた側が主導する前提で、
オフショアは「サービス層を呼ぶ」「レスポンスを表示する」部分がメイン。

★理解しておくべきこと

  • Chroma の役割:
    • 「分析コメント」や「メタ情報」を後から検索するためのストレージとして使っていること
  • あなたが決めた Chroma の設計ルール:
    • collection
    • doc_id の生成ルール
    • metadata に必ず含めるキー(session_id, used_tables など)
  • 「upsert は冪等(idempotent)」であること
    → 同じ doc_id に対する再アップサートは「上書き」であり、重複を増やすものではないこと

Offshore-Task 4:Dev-Portal の入力UI実装(フロント)

目的
あなたが設計した Dev-Portal のモデル/フィールド/ビュー入力画面を、Figma と OpenAPI に忠実に実装する。

やってもらうこと

  • Figma.make の画面定義を見ながら、各画面のコンポーネントを実装
    • モデル一覧
    • モデル詳細(フィールドタブ/ビュータブ)
    • View Common / View 詳細編集画面
  • あなたが用意した Screen↔API マッピングに従い、Dev-Portal API に接続
    • GET /portal/models
    • GET /portal/models/{id}
    • POST/PATCH /portal/models/{id}
      など実際のパスに合わせて
  • フォームのバリデーション・エラー表示を実装
    • 必須項目
    • 型のチェック(数値/選択肢など)

★理解しておくべきこと

  • Dev-Portal の目的:
    • Odoo の「構造(モデル/フィールド/ビュー)」を
      人間とAIの両方がわかりやすい形で管理するポータルであること
  • portal_model, portal_field, portal_view_* などのテーブルは、**業務DBではなく“メタDB”**であること
  • i18nの扱い:
    • ラベル・ヘルプ等が *_i18n の JSONB で管理されていること
    • 翻訳は専用のパイプラインがあるので、フロント側で勝手に翻訳しないこと

Offshore-Task 5:Dev-Portal UI → 翻訳/Chroma/nlq-dev への配線

目的
Dev-Portal 上で入力されたメタ情報が、翻訳 → Chroma → nlq-dev まで流れるように、UIからパイプラインを「発火」できる状態にする。

やってもらうこと

  • Dev-Portal UI から翻訳ジョブをキックする操作を実装
    • 例:対象モデル/フィールドを選択して「翻訳キューに送る」ボタン
    • POST /translate/run 等、既存のOpenAPIに従う
  • Chroma package / upsert 操作をフロントから起動できるようにする
    • 例:管理画面から POST /chroma/packagePOST /chroma/upsert を叩く
  • nlq-dev から見たときに、
    • 必要なメタ情報(モデル説明/フィールド説明)が Chroma に入っている状態が作れることを確認

★理解しておくべきこと

  • 翻訳パイプラインの概念:
    • Dev-Portal → portal_translateportal_chroma_doc → Chroma の流れ
  • 「UIからは“トリガーを押すだけ”」であり、
    • パイプラインの中身(SQLやバッチ処理)は Raqqa 側が責任を持つ領域であること
  • nlq-dev は、このメタ情報を RAG の文脈として参照するだけで、
    メタ情報の生成や品質は Dev-Portal 側の責任範囲であること

Offshore-Task 6:k8s/overlays/dev・prod および AWS 移行

目的
あなたが作った k8s/base をベースに、実際の dev/prod 環境で nlq-dev + Dev-Portal を動かす。

やってもらうこと

  • k8s/overlays/dev の整備
    • レプリカ数、リソースリクエスト/リミット
    • dev 用 Ingress(dev.example.com 的な)
    • 簡易な Basic 認証 or IP制限(必要なら)
  • k8s/overlays/prod の整備(AWS EKS 前提)
    • Ingress に ALB コントローラ用の annotation 付与
    • ACM 証明書 ARN の設定
    • secret-api-db, secret-api-auth などシークレット連携
  • AWS 上での動作確認
    • kubectl apply -k k8s/overlays/prod がエラーなく通る
    • /health/ready が 200 を返す
    • 簡単な NLQ クエリ〜分析まで通る

★理解しておくべきこと

  • base は Raqqa 側がコントロールし、
    overlays でだけ環境依存の要素を変えるという方針
  • AWS EKS / ALB / ACM の基本構造
    • EKSクラスター
    • ALB Ingress Controller
    • ACM 証明書
  • シークレットや認証情報は Gitにコミットしない こと
    → マニフェストは secretRef まで、値は別管理であること

Offshore-Task 7:CI/CD スケルトン構築(最後の仕上げ)

目的
「新規顧客プロジェクトが始まったときに、このテンプレをコピーすれば環境が立ち上がる」状態を CI/CD でサポートする。

やってもらうこと

  • GitHub Actions(または他CI)で、
    • nlq-devdev-portal のビルド&テストジョブを用意
    • Docker image をコンテナレジストリ(GHCR / ECR)に push
  • 手動トリガー(workflow_dispatch)で、
    • 特定のバージョンを dev / prod にデプロイするジョブを用意
      (ここは skeleton でOK。kubectl apply or ArgoCD sync のラッパ程度)
  • CI でやりすぎないこと:
    • この段階では「最低限ビルド+テスト+デプロイ」が出来ればよく、
    • 複雑なマトリクスやマルチテナント自動構築まではやらない

★理解しておくべきこと

  • 「CI/CD の設計権限は Raqqa 側にあり、
    今回は “スケルトンを作る” ところまでがスコープ」であること
  • GitHub Actions の基本:
    • workflowのトリガー
    • job / step / runnerの概念
    • シークレットの扱い
  • 将来、「顧客ごとの overlay」「Helm values」などを CI で扱う可能性はあるが、
    今回のスプリントではそこまで踏み込まないこと

こんな感じで分けておくと、

  • あなた側:設計・スキーマ・バックエンド骨格・base k8s
  • オフショア:フロント実装・overlays・AWS・CI/CD

という役割がかなりクッキリします。

このうち、「まずは Offshore-Task 1〜3 だけやってもらう」といった形で
段階的に依頼していくのもアリですし、各タスクを PR 番号(NLQ-PR-01 みたいな)に紐づけても運用しやすいと思います。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です