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

工程7最終: DeciKG


Semantic Search / Dynamic Cypher / Confidence

前提:工程7-1(完了)

I/O契約(JSONL)固定 + 正規化DoD

  • rows は常に list[object]
  • confidence は常に object
  • rows>0 の場合、各rowに analysis_pack が必ず付与される
  • 変更は追加のみ(削除/rename/型変更/required化禁止)

7-1 DoD確認 jq(あなたが実行してOKになったやつ)

jq -e '
  def ok_rows: (.rows|type)=="array" and ((.rows|all(type=="object")));
  def ok_conf: (.confidence|type)=="object";
  def ok_ap:   ((.rows|length)==0) or ((.rows[0].analysis_pack|type)=="object");
  if ok_rows and ok_conf and ok_ap then
    "OK DoD: rows=list[object], confidence=object, analysis_pack per-row when rows>0"
  else
    error("NG DoD")
  end
' /tmp/responses_rank.check.jsonl

工程7-2:Semantic Search Targets(Chroma等)導入(LLMなしでOK)

目的

  • 固定テンプレCypherの「前段」に semantic_search_targets を追加し、
    • slots不足/曖昧なときに、候補(opp_id / rep_id / company_id等)を補完できるようにする
  • 検索が弱い/空なら従来ルール(固定Cypher)へフォールバック(安全)

実装ポイント(設計)

  • 追加のみで入れる(既存の pack/registry の固定Cypherを壊さない)
  • diagnostics.assist.semantic_search に結果を記録(add-only)
    • enabled, provider, query, hits, picked, elapsed_ms, reason

DoD(7-2)

  • semantic_search が ON のとき:
    • diagnostics.assist.semantic_search.enabled=true
    • hits>=0 が入り、落ちても エラーにせず warnings に記録して続行
  • OFF のとき:従来と同じ(出力契約・結果が変わらない)

実行コマンド(例:rank)

cd ~/DeciKG/decikg

python -m decikg.graph_query.run_jsonl \
  --in  ./requests_opportunity_pipeline.rank.jsonl \
  --out /tmp/responses_rank.7_2.jsonl \
  --progress --log-level INFO \
  2>&1 | tee /tmp/run_rank.7_2.log

DoD確認 jq(7-1 + assist確認)

jq -e '
  def ok_rows: (.rows|type)=="array" and ((.rows|all(type=="object")));
  def ok_conf: (.confidence|type)=="object";
  def ok_ap:   ((.rows|length)==0) or ((.rows[0].analysis_pack|type)=="object");
  def ok_assist:
    (.diagnostics.assist.semantic_search? | type) == "object"
    or true; # assistは optional(offでもOK)
  if ok_rows and ok_conf and ok_ap and ok_assist then
    "OK 7-2: contract ok (+assist optional)"
  else
    error("NG 7-2")
  end
' /tmp/responses_rank.7_2.jsonl

工程7-3:Dynamic Cypher(制約付きText-to-Cypher)を “fallback専用” で導入

目的

  • 既知 action_xmlid + query_key は従来通り固定Cypher(最優先)
  • 以下の場合のみ “fallback” として動的生成を使う:
    • unknown_action_xmlid / unknown_query_key
    • 必須slots不足
    • もしくは semantic_search しても補完できない

制約(安全ガード)

  • dynamic生成は必ず:
    • MATCH/RETURN のみ(CREATE/DELETE/MERGE禁止)
    • LIMIT 強制
    • timeout_ms 強制
    • allowlist(ラベル/リレーション)以外は参照禁止
  • 失敗時は skipped or error で返し、契約は維持

DoD(7-3)

  • dynamic生成が走ったら:
    • diagnostics.assist.dynamic_cypher.used=true
    • diagnostics.assist.dynamic_cypher.reason が入る(why fallback)
  • 生成されたCypherの先頭が危険語を含まない(検査)
  • 失敗しても runner は落ちず、1行=1レスポンスを維持

DoD確認 jq(fallbackが発生したときだけ)

jq -e '
  if (.diagnostics.assist.dynamic_cypher.used? == true) then
    "OK 7-3: dynamic_cypher used reason=" + (.diagnostics.assist.dynamic_cypher.reason|tostring)
  else
    "OK 7-3: dynamic_cypher not used"
  end
' /tmp/responses_rank.7_3.jsonl

工程7-4:Confidence Scoring を 100/60系へ拡張(add-only)

目的

  • confidenceobjectのまま(7-1維持)
  • 0..1の confidence.score を壊さずに、追加で 0..100 の評価を載せる

推奨スキーマ(add-only)

  • confidence.score : 0..1(既存)
  • confidence.score100 : 0..100(追加)
  • confidence.badges : string[](追加)
  • confidence.notes : string(追加)
  • confidence.components : object(追加、根拠内訳)

DoD(7-4)

  • confidence は常に object
  • confidence.score100 が存在する場合は 0..100 int に丸められている
  • confidence.badges は list[string]

DoD確認 jq(score100の検査)

jq -e '
  def ok_conf: (.confidence|type)=="object";
  def ok_score100:
    (.confidence.score100? == null)
    or ((.confidence.score100|type)=="number" and (.confidence.score100>=0) and (.confidence.score100<=100));
  def ok_badges:
    (.confidence.badges? == null)
    or ((.confidence.badges|type)=="array" and (.confidence.badges|all(type=="string")));
  if ok_conf and ok_score100 and ok_badges then
    "OK 7-4: confidence extended (add-only)"
  else
    error("NG 7-4")
  end
' /tmp/responses_rank.7_4.jsonl

ontract: confidence extension (7-4) v0.1 (add-only)
Input(既存の期待)

response.confidence は無い場合がある

ある場合は object で、confidence.score(0..1 float)が入ることがある

Output(7-4で保証すること)

response.confidence は 常に object(無い場合は {})

confidence.score は 既存互換(触らない。無ければ追加もしない)

confidence.score100(追加)は以下条件でのみ付与:

confidence.score が number のとき

score100 = clamp(round(score*100), 0, 100)(int)

confidence.badges(追加)は以下条件でのみ付与:

付与するなら list[string](空配列OK)

confidence.notes(追加)は任意 string

confidence.components(追加)は任意 object

Recommended badges(最小)

fixed_spec / semantic_search_filled / dynamic_fallback / rows_nonempty / neo4j_ok

DoD jq(あなたのままでOK)
jq -e '
  def ok_conf: (.confidence|type)=="object";
  def ok_score100:
    (.confidence.score100? == null)
    or ((.confidence.score100|type)=="number" and (.confidence.score100>=0) and (.confidence.score100<=100));
  def ok_badges:
    (.confidence.badges? == null)
    or ((.confidence.badges|type)=="array" and (.confidence.badges|all(type=="string")));
  if ok_conf and ok_score100 and ok_badges then
    "OK 7-4: confidence extended (add-only)"
  else
    error("NG 7-4")
  end
' /tmp/responses_rank.7_4.jsonl

工程7-5:E2E(7-1〜7-4を一括で守る回帰DoD)

目的

  • どの経路(固定Cypher / semantic補完 / dynamic fallback / postprocess)でも
    • JSONL 1行=1レスポンス
    • 7-1契約が崩れない
    • assistやconfidence拡張が入っても破壊しない

一括DoD jq(最終)

jq -e '
  def ok_rows: (.rows|type)=="array" and ((.rows|all(type=="object")));
  def ok_conf: (.confidence|type)=="object";
  def ok_ap:   ((.rows|length)==0) or ((.rows[0].analysis_pack|type)=="object");
  def ok_conf_ext:
    (.confidence.score100? == null)
    or ((.confidence.score100|type)=="number" and (.confidence.score100>=0) and (.confidence.score100<=100));
  if ok_rows and ok_conf and ok_ap and ok_conf_ext then
    "OK 7-5: contract+confidence-ext maintained"
  else
    error("NG 7-5")
  end
' /tmp/responses_rank.final.jsonl

実行コマンド(あなたが欲しい “detail check” 版)

あなたの指定の形で、detail用のチェック実行を置いておきます(ログも保存):

cd ~/DeciKG/decikg

python -m decikg.graph_query.run_jsonl \
  --in  ./requests_opportunity_detail_gold3.jsonl \
  --out /tmp/responses_detail.check.jsonl \
  --progress --log-level INFO \
  2>&1 | tee /tmp/run_detail.check.log

そして、最終DoD(7-1契約)確認:

jq -e '
  def ok_rows: (.rows|type)=="array" and ((.rows|all(type=="object")));
  def ok_conf: (.confidence|type)=="object";
  def ok_ap:   ((.rows|length)==0) or ((.rows[0].analysis_pack|type)=="object");
  if ok_rows and ok_conf and ok_ap then
    "OK DoD: rows=list[object], confidence=object, analysis_pack per-row when rows>0"
  else
    error("NG DoD")
  end
' /tmp/responses_detail.check.jsonl

工程7-6:NL → Cypher生成 → 実行 → 説明(対話入力の完成)

ゴール(7-6の到達点)

1つの 自然言語質問(question) を入力すると、内部で:

  1. semantic search(関連ビュー/クエリ候補/制約を取得)
  2. 制約付き Cypher 生成(安全な範囲で text-to-cypher)
  3. Neo4j 実行(timeout/limit/エラー分類)
  4. 結果の説明(根拠+要約+confidence)

を行い、出力は GraphQueryResponse v0.1(追加のみ) を守る。


“残りの部品”一覧(7-6で実装するもの)

A. 入力の入口(質問を受ける契約)

  • GraphQueryRequest に question を追加(add-only)
    • 例:request.question: str(必須ではなく optional にして段階移行)
  • 既存の action_xmlid/query_key/slots が無い場合でも、
    • question から 候補の action_xmlid/query_key を推定できる導線を作る

ここを「別コマンド/別モジュール」にすると運用が崩れやすいので、最初は JSONL input でも question を扱える形が良いです。


B. semantic_search(候補を引く)

  • semantic_search_targets(例)
    • GraphQuery “packs” のメタ(action_xmlid, query_key, 説明文, required slots, shape)
    • DomainGuide / view catalog(将来)
  • 最初は軽量でOK:
    • pack registry をテキスト化してベクトル化(Chroma or ローカルTF-IDFでも可)
  • 出力は diagnostics.assist.semantic_search に集約(add-only)
    • 例:used, hits_total, hits_kept, top_hits[{action_xmlid, query_key, score}]

C. dynamic_cypher(制約付き生成)

ここが7-6の核です。

  • “候補が決まった場合”:
    • 既存の テンプレCypher+slots 方式を優先(安定)
  • “候補が決まらない/slots不足”の場合だけ:
    • LLMでCypherを生成(ただし必ず制約/検証を通す)

最低限の制約(MUST):

  • READ ONLY(CREATE/MERGE/DELETE/SET/CALL dbms… を禁止)
  • 単一ステートメント
  • 必ず LIMIT(options.limit を上限に)
  • 許可ラベル/許可リレーション/許可プロパティの allowlist
  • パラメータ化優先(文字列直書きしない)

生成の結果は:

  • diagnostics.cypher に入る(include_cypher時)
  • diagnostics.assist.dynamic_cypher に「使った/使わない/理由/elapsed_ms」

D. slot_filling(必要スロットを埋める)

“質問→実行”で破綻しがちなのがここ。

  • packが要求する required_slots があるのに埋まらない場合:
    • 7-6では 2段階にするのが現実的
      1. extract(質問から company_id / opp_id / period 等を正規表現 + 辞書で拾う)
      2. 足りなければ missing_required_slots で返す(対話UIなら追加質問につなぐ)

LLMに全部やらせるより、まず deterministic に拾ってから不足だけ聞く方が、回帰が安定します。


E. 実行(Neo4jClientのラップ強化)

すでに neo4j_client.py はあるので、7-6はここを “プロダクト仕様” に寄せます。

  • timeout/limit は options.timeout_ms / options.limit を最優先
  • Neo4j例外を reason_code に分類
    • neo4j_error / timeout / invalid_params など
  • 実行結果 row_count を diagnostics.row_count
  • 必要なら EXPLAIN/PROFILE は debug時だけ(通常は不要)

F. 説明生成(explanation)

説明は add-only で、出す場所を決めます。

推奨:

  • 各 row に analysis_pack.explanation(string)
  • もしくは top-level analysis_pack を正式フィールド化(今はdynamic注入もあるので整理)

説明の最低仕様:

  • 何を返したか(要約)
  • 根拠(countsや代表的なevidence)
  • 注意(欠損/推定/空ヒット)
  • confidence と整合する(lowなのに断定しない)

G. confidence_scoring(100/60等の拡張)

現状は confidence: {level, score(0..1)} があるので、追加で:

  • confidence.score_100(0..100)を追加(add-only)
  • confidence.notes(list[str])追加
  • ルール:
    • evidence量(counts)+ 生成経路(テンプレかLLMか)+ missing補正 で決める

7-6のDoD(回帰可能な形)

入力(例)

JSONLで question を渡せる:

{"request_id":"q1","question":"直近30日でメール反応が多い案件を上位10件","params":{"now":"2026-02-10"}}

出力(必須)

  • 7-1の形は絶対に崩さない(rows/confidence/analysis_pack)
  • diagnostics.assist.semantic_search が出る(少なくとも used/reason)
  • diagnostics.cypher が(include_cypher=trueのとき)出る
  • analysis_pack.explanation が rows>0 のとき全rowに入る

成功判定(例)

  • status=ok で rows が返り、explanation が付く
  • 候補不明/不足なら status=error or skipped + missing_required_slots で落ち方が安定

実装タスクの切り方(7-6-1〜7-6-4)

7-6-4: explanation + confidence(100点換算) + E2E(質問入力)追加

7-6-1: requestに question 追加 + registryで経路分岐(テンプレ優先)

7-6-2: semantic search(pack registryのベクトル化)+ assist診断

7-6-3: dynamic cypher generator + guard/allowlist + reason_code分類


Comments

コメントを残す

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