/** * 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'] ); } } } } } } } } DeciKG 開発工程 – Raqqa

DeciKG 開発工程

開発ステップ(確定案)

工程1:DomainGuide Pack(YAML)Import と反映(最優先)

目的

  • **View単位(action_xmlid単位)**の “開発の入口” を DeciKG 側に取り込み、以後の開発は このPackを前提に進める。
  • Dev-portal が更新したYAMLを 再importで安全に更新できるようにする(冪等)。

やること(DeciKG側)

  1. Pack保存先を決める(推奨:Postgresに原本、Neo4jに反映)
  • Postgres: decikg_view_pack(原文yaml_text / 正規化json_doc / source_hash / action_xmlid PK)
  • Neo4j: (:ViewCommon {action_xmlid,...})-[:HAS_GUIDE]->(:DomainGuide {action_xmlid,...})
  1. Importer(バッチ)実装
  • 入力:packs/*.yaml(今回作ったファイル群)
  • 処理:
    • YAML parse → 正規化(json_doc)
    • action_xmlid で UPSERT
    • sync.source_hash が同じなら NOP、違えば更新
    • Neo4j へ反映(MERGE + SET)
  1. 更新のDoD(必須)
  • 同じYAMLを2回 import → 2回目はNOP
  • YAMLの一部を修正 → importで更新される(source_hash差分)

ここまでで「Dev-portalが吐いたPackをDeciKGに取り込んで、開発開始できる」状態が成立します。


工程2:観測データ(fact/odoo-like)Import(LLMなし)

目的

  • 推論やランキングの根拠となる “観測” をグラフに載せる。

やること

  • 入力:import_dir/materials/*.csv または *.jsonl
  • 実装:kind -> handler のレジストリ方式(辞書)
  • まず最低限:
    • Company / Period / SalesRep / Opportunity / ProposalSignal / Outcome / EmailEvent / PhraseStat
    • (必要に応じて)CRM系(Lead/Message/Activity)・SAPB1(Order/Line)

DoD

  • import後、Neo4jで基本リレーションを確認できる(例:Opportunity→Signal/Outcome、Opp→EmailEvent、Opp→PhraseStat)

工程3:固定Cypher(View単位)で /graph/query 相当の結果を返す(LLMなし)

※あなたの方針どおり「追加開発は基本ここで完結」できる骨格にします。

目的

  • Pack(action_xmlid) を入口にして、固定Cypherで結果を返す。
  • 外部VPC問題があるので API前提ではなくバッチで回る形を先に作る。

実装(確定)

  1. QueryEngine
  • run(action_xmlid, query_key, slots, options) -> GraphQueryResponse
  • query_key は最初は固定で良い(例:rank, detail, summary, calibration
  1. Registry(固定Cypher)
  • action_xmlid ごとに
    • required_slots
    • shape_normalized
    • cypher_template
    • postprocess(集計や整形)
      を登録
  1. 実行形態(初期はバッチ)
  • 入力:requests.jsonl(GraphQueryRequestの配列)
  • 出力:responses.jsonl

ここで LLMなしで Q1/Q2 相当を返せる状態が完成します。

DoD

  • 例の4 pack について、各1本ずつ固定クエリが動く
    • opportunity_pipeline(ランキング)
    • opportunity_detail(1件詳細+根拠)
    • customer_360(顧客サマリ)
    • rep_calibration(較正レポート)

工程4:推論(受注確度)と根拠(explanation_json)を“ルール”で実装(LLMなし)

目的

  • 「担当者の勘」を 再現可能なルールに落とし、結果と根拠を構造化して返す。

やること

  • win_prob.py(ルール)
    • 入力:latest_signal / comms集計
    • 出力:probabilityfactors[](寄与要因の配列)
  • QueryEngineはランキング時にこのルールを適用して analysis_pack に格納

DoD

  • “理由付きランキング” が返る(上位ほど要因が明確)
  • 同じデータなら結果が再現される(決定論)

工程5:graph/query 契約(追加のみ)を「ファイル入出力」として固定(最初から)

あなたの環境制約(DeciKGがVPC外、叩けない)に合わせて、APIではなく契約として固定します。

GraphQueryRequest(v0.1)

  • action_xmlid(必須)
  • query_key(必須:rank/detail/summary/calibration など)
  • slots(期間など)
  • options(dry_run, limit など)

GraphQueryResponse(v0.1)

  • status / reason_code
  • shape_normalized
  • data(columns/rows でも items でもOK、ただし固定)
  • analysis_pack(後から拡張)
  • confidence(後述。まずはルールで付与)

DoD

  • requests.jsonl → responses.jsonl が1コマンドで生成できる

LLM推論拡張(後工程:ただし“器”だけ先に入れる)

あなたの提案(Flexibility / Grounding)を 後工程に回しますが、YAMLのschema上は最初から任意項目として受け取れるようにしておきます(追加のみのため)。

工程6(後):llm_reinforcement_contract をPackに追加(enabled=false)

  • importer は 保存するだけ(解釈しない)
  • DeciKGエンジンは 無視(enabled=false の間)

工程7(後):Semantic Search / Dynamic Cypher / Confidence(LLMあり)

    1. semantic_search_targets → ベクトル検索(Chroma等)
    1. dynamic_generation_rules → 制約付きText-to-Cypher(fallbackのみ)
    1. confidence_scoring → 100/60などのスコア+注釈

旧工程

  工程0:契約固定(KPI追加の“入口”を固定する)

目的:追加開発を工程1+2に閉じるための“枠”を作る

やること

  • domain_guideファイル仕様を固定(domain/module/locale + decikg.sql_contract など)
  • /graph/query のリクエスト・レスポンス契約を固定(追加のみ)
  • pattern_key命名規約を固定(例:kpi::opportunity_pipeline::win_rate

実装

  • api/app/services/contracts/domain_guide_schema.py(軽いバリデーション)
  • api/app/services/contracts/query_contract.py(shape_normalizedの型)
  • pattern_key の規約コメント(READMEでもOK)

DoD

  • 新KPIを追加しても工程3以降に影響しない契約が固まっている

工程1A:YAML(domain_guide)をファイルで管理し、Neo4jへ取り込み

目的:企業差分の“意味”をdomain_guideに閉じ込める経路を確立(=KPI追加の第一手)

やること

  • domain_guides/*.yaml を読み込み → Neo4j (:DomainGuide) にMERGE
  • company_id で差分を持てるようにする(ここが肝)
    • 例:DomainGuideに tenant/company_id を持たせる、または overrides.company_id を持たせる

おすすめ設計(差分の閉じ込め先)

  • DomainGuideノードに以下を持つ:
    • domain(例:opportunity_pipeline)
    • company_id(例:demo_company_001)※複数企業対応の布石
    • yaml_text(raw)
    • json_doc(正規化したJSON)

実装

  • api/app/services/materials_loader.py(yamlロード)
  • api/app/services/importer.pyimport_domain_guides(import_dir) 追加

DoD

  • domain_guide YAML を増やすだけで Neo4j に意味が載る(idempotent)

工程1B:JSONL(観測データ)をNeo4jへ取り込み(必要ならkind追加)

目的:KPIの根拠となる“観測”を入れる(KPI追加の第二手)

やること

  • kind最小セットで import(Company/Rep/Period/Opp/Signal/Outcome)
  • KPI追加で必要なら kind を追加しても handler追加だけで済む構造にする

実装

  • api/app/services/importer.py
    • kind→handlerのレジストリ方式(switchではなく辞書)
  • api/app/routers/admin_import.py
    • POST /admin/import(import_dir指定)

DoD

  • import後に指定のリレーションが確認できる(あなたのDoDそのまま)

工程2:固定Cypher(pattern_keyごと)で結果を返す(KPI追加はここでCypher追加)

目的:追加KPI開発を工程2で完結できるようにする(Cypher追加OK)

工程2のキモ(追加開発を工程2に閉じる条件)

Cypherテンプレは 必ず domain_guide を参照します。

  • company_id + domainDomainGuide を引く
  • sql_contract(distinct_keyやforbid)や tables/join/metrics/dimensions を参照して
    • クエリの粒度やDISTINCTを守る
    • 期間・repなどのフィルタの列をブレさせない

※ここは「Cypher自体は追加していい」方針なので、
“domain_guideを読んで自動生成”までは不要です。
ただし **テンプレの中で domain_guide を読む(参照する)**のをルール化します。
これで「企業差分=domain_guide」に閉じ込められます。

実装

  • api/app/services/query_engine.py
    • run_pattern(pattern_key, slots)->ShapeResult
    • patternごとに cypher_template + required_slots + shape を登録
  • api/app/services/domain_guide_repo.py
    • get(company_id, domain)->DomainGuideDoc
  • api/app/routers/graph_query.py
    • POST /graph/query
    • 最初は pattern_key必須(自然文分類は工程5以降)

DoD

  • Q1/Q2が返る(現状のDoDを維持)
  • さらに KPIパターンを1本追加すると工程1+2だけで動くことをデモできる
    • 例:kpi::customer_360::email_count_last_7d みたいな“追加KPI”を1本作って証明

今回の工程における「KPI追加」の具体手順(追加開発時)

あなたの狙い通り、追加開発ではこれだけで済むようにします:

  1. 工程1Adomain_guides/<company>/<domain>.yaml を追加 or 更新
  2. 工程1B:必要なら観測データkindを追加してimport(または既存データで足りるなら不要)
  3. 工程2pattern_registry に Cypherテンプレを1本追加(pattern_key追加)

👉 これで 工程3以降を触らずに新KPIが返せる。

工程3:暗黙知の核心(予測+理由=Explanationの最小実装)

目的:「理由付き」の最小実装を“ルールベースで再現可能”にする

やること

  • proposal_signal特徴量から predicted_probability を出す(ルール)
  • 寄与特徴を explanation_json に格納(文章ではなく構造化でOK)
  • Q2結果に同梱

実装内容

  • api/app/services/win_prob.py(新規)
    • predict(signal)->{prob, factors[]}
  • query_rank_oppslatest_signal を使って説明を生成(オンザフライでOK)

DoD

  • “理由付きランキング” が返る(上位ほど理由が明確)
  • 予測ロジックが固定で再現可能

工程4:較正(当たる/当たらないを“補正”として出す)

目的:人格評価ではなく「傾向/補正」として扱う

やること

  • rep別に自己申告と実結果を集計
  • over/under を数値で返す(サンプル少ない場合は抑制)

実装内容

  • query_engine.py に追加
    • pattern::calibration_report
    • pattern::rep_bias_rank
  • 指標:brier / calibration_bias / hit_rate_at_0_7 / sample_size

DoD

  • 「佐藤さんは高めに言う傾向(+0.08)」的な結果が返る
  • 少サンプル時は「不確実」と明示

工程5:自然文→pattern選定(LLM無しで開始、LLMは後)

目的:UIから自然文で投げても“壊れない”

やること

  • まずはルール(キーワード)で pattern を選ぶ
  • slot抽出は最初は期間だけでもOK

実装内容

  • api/app/services/intent_router.py
    • pick_pattern(question)->pattern_key
    • extract_slots(question)->slots

DoD

  • pattern_key無しでもQ1/Q2/Q4系が動く

工程6:analysis_pack の確定出力(nlq-devが使える形に)

目的:nlq-devがコメント生成できる“材料”をDeciKGが保証する

やること

  • /graph/query レスポンスに analysis_pack を追加(追加のみ)
  • evidence_trace(簡易:参照したsignal/outcome/featureのID)

実装内容

  • api/app/services/analysis_pack_builder.py
  • shape_normalized + findings + evidence_trace + timings 等

DoD

  • nlq-devが analysis_pack だけでコメント生成できる

工程7:Dev-Portal “燃料”差し替え準備(今はダミー)

目的:後でdev-portal exportに置換できる状態にする(今回は連携しないが壊れない設計)

やること

  • materials.jsonl / domain_guides.yaml の契約を固定
  • importerを kind追加しやすい設計(handler登録方式)にする

実装内容

  • api/app/services/importer.pyhandlers: dict[kind]->func に寄せる
  • DomainGuide 取り込みは既に工程1Aで成立済みなので、後は差し替えだけで済む

DoD

  • ダミーmaterialsでもimportが壊れない
  • 後でdev-portalが吐くJSONL/YAMLに置換できる見通しが立つ

重要:Chromaの位置づけ(今回の工程では必須ではない)

  • 工程1AのYAML取り込みで「後でChromaに詰める材料」は確保できます
  • 工程2〜6は Chromaなしで成立(固定Cypher+構造化理由+analysis_pack)
  • Chromaが必要になるのは「自然文分類をLLM/ベクトルで強化」「domain_guide引用」「説明の文章化」をやる段階

Comments

コメントを残す

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