/** * 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 GraphQuery(search/detail/rank)+ nlq-dev 連携方針 – Raqqa

DeciKG GraphQuery(search/detail/rank)+ nlq-dev 連携方針

0. ゴール(役割分担)

  • DeciKGquestion を(LLM or ルールで)パラメータ化 → spec選択 → Cypher生成 → 実行 → rows返却
    • 返すもの:Cypher / params / rows / diagnostics(分析文章は返さない)
  • nlq-dev:DeciKGの rows を分析して文章化(必要なら追加質問を作って Dev-Portal へ)
  • 旧案の CYPHER_THEN_SQL「CypherでID解決 → SQLで詳細取得/集計」 のときに意味が出る。
    今回の当面運用は DeciKG=Cypher+データまで、分析はnlq-dev。

1. ここまでやったこと(時系列)

1) レスポンス整形の安定化(schemas.py)

  • response_to_dict()add-only の補正を追加
    • rows[*].analysis_packnull/欠損なら {} を強制(7-5)
    • analysis_pack.explanation を(LLMなしで)最小付与(7-6)
    • confidence を常に object 形状に正規化(Engineering-5)
    • dynamic fallback cypherdiagnostics.assist.dynamic_cypher にある場合、diagnostics.cypher/paramsミラー(デバッグ容易化)
  • jqで合格判定:diagnostics.cypher が埋まっていることを確認済み(別ファイルでOK表示)

2) Neo4jデータ実態の確認

  • Labels / relTypes / counts 確認済み
    • Labels: Company, Opportunity, Partner, SalesRep, User, EmailEvent, PhraseStat, ProposalSignal, Outcome, DomainGuide, ViewCommon
    • RelationshipTypes: HAS_CUSTOMER, OWNED_BY, HAS_EMAIL_EVENT, HAS_PHRASE_STAT, HAS_SIGNAL, IN_PERIOD, IS_USER, etc.
  • Opportunity は company_id="c001" に 300件

3) 「佐藤」検索が当たらない原因の切り分け

  • User に「佐藤 奈々」がいるが、**Userが孤立(関係0)**の例が多く、Opportunity→SalesRep→User の経路で当たらなかった
  • 対策として、孤立Userを login から rep_id_guess=u07 を生成し、SalesRep(company_id, rep_id) を MERGE して (:SalesRep)-[:IS_USER]->(:User) を付与
  • さらに、PoC案件の一部 Opportunity を OWNED_BY -> u07 に差し替えて、「佐藤」→案件 がヒットする状態を作成
  • Neo4j直接クエリでは、以下が成立:
    • User(name contains 佐藤)SalesRep(rep_id=u07)Opportunity(OWNED_BY u07) が 2件ヒット(op0006/op0011)

2. 現状のAPI検証結果(DeciKG /graph/query)

実行したcurl

curl -sS http://127.0.0.1:${API_PORT:-8000}/graph/query \
  -H 'Content-Type: application/json' \
  -d '{
    "request": {
      "request_id":"chk_sato_search_01",
      "question":"佐藤の案件を10件",
      "slots":{"company_id":"c001"},
      "params":{"q":"佐藤","limit":10},
      "options":{"include_cypher":true,"include_params":true}
    }
  }' \
| jq -c '{status, reason_code, cypher:(.diagnostics.cypher//null), params:(.diagnostics.params//{}), rows_len:(.rows|length), rows:(.rows[0:2])}'

結果(重要)

  • status=ok, reason_code=ok
  • diagnostics.cypher が “Opportunity.name のみ CONTAINS” のシンプル版になっている: MATCH (n:Opportunity {company_id: $company_id}) WHERE toLower(n.name) CONTAINS toLower($q) RETURN ... LIMIT $limit
  • params={"limit":10,"timeout_ms":800}(company_id と q が出ていない点も注意)
  • rows_len=0(0件)

解釈

  • DeciKG側の spec 推定が “fuzzy横断テンプレ” を選んでいない
  • かつ、Cypherが Partner/SalesRep/User を見に行っていないので「佐藤」を拾えない
  • LIMIT $limit を使っている(cypher-shellではLIMITに変数が使えない環境もあるが、API実行側Neo4jでは通ることが多い。ただし安全策としてDeciKG側でリテラル化を推奨)
  • paramsに company_id/q が diagnostics.params に反映されていない(response_to_dict のミラーは limit/timeout のみ入っている)
    → どこで params を組んでいるか確認が必要

3. 設計方針(3軸テンプレ)

「検索」「深掘り」「評価」の3軸で query_key / spec を固定する。

A) search.fuzzy_match(探索の起点)

  • LLM(またはルール)が吐く params(最小):
    • { "entity_type": "Opportunity", "q": "...", "limit": 10 }
  • Cypherは Opportunity/Partner/SalesRep/User を横断して CONTAINS で当てる(今回の目的:固有名詞・担当者名で見つける)

B) detail.trace_impact(周辺を展開)

  • { "opp_id": "...", "depth": 1 or 2 }
  • 1〜2 hop で EmailEvent/PhraseStat/Signal/Outcome などをまとめて返す(直積回避のため CALL { } 集計)

C) rank.bottleneck_priority(重み付け評価)

  • { "metric": "risk", "limit": 5 }
  • 例:signal/email/phrase を重み付けして score を作り ORDER BY

DeciKG 工程(4つ)

工程D1: データ健全性(Neo4jが正しいか)

目的:グラフ自体は答えを持っていることを証明する(DeciKGの外)。

  • labels / rel types / counts
  • “佐藤→案件” が Neo4j直で2件以上返る

→ ここがOKなら「データが無い」は否定できる。


工程D2: Spec推定(question → spec/intent の決定)

目的:**LLM(またはルール)が「どのテンプレを使うか」**を当てられているか確認。

  • 入力:question="佐藤の案件を10件"slots.company_id
  • 出力:intent=search.fuzzy_match(最低でも search 系)
    かつ params.q="佐藤", params.limit=10 が生える(または既存 params を維持)

→ ここが壊れてると、以降は全部ズレる。


工程D3: Cypher生成(spec + params → cypher)

目的:正しいCypherテンプレが選ばれているか確認。
期待:Opportunity.name だけじゃなく、最低でも以下のどれかが入る

  • HAS_CUSTOMER(Partner.name 検索)
  • OWNED_BYIS_USER(担当者名検索)

さらに重要:LIMIT は Neo4j都合で “変数limit” はNGなので、DeciKG側は

  • LIMIT $limit を使う(パラメータ化)
    ※あなたのCLI検証で通ってる方式

→ ここで “狭いCypher” ならテンプレ側の問題。


工程D4: 実行+レスポンス整形(neo4j run → response_to_dict)

目的:rowsが返る + diagnostics mirror が入る(あなたが直したところ)。

  • rows_len>0
  • diagnostics.cypher / diagnostics.params が埋まる
  • 追加で analysis_pack が per-row object になっている

→ ここがOKなら、DeciKGは「Cypherとデータを返す」責務を満たした状態。


結論:分けるなら “D2→D3” が境界

いま詰まってるのはほぼここです。

  • Neo4j直ではヒットする(D1 OK)
  • APIは Opportunity.name CONTAINS $q しか出していない(D3 NG)
  • なので Spec推定 or テンプレ選択(D2) が間違ってるか、
    search.fuzzy_match のテンプレ自体(D3) が未実装/古い

次スレに持っていく「DeciKG工程テンプレ(短文)」

次スレ冒頭はこれで十分です:

  • D1: Neo4j直で “佐藤→案件” は2件返る(OK)
  • D2: question→spec 推定が search.fuzzy_match になっているか要確認(未確認)
  • D3: 生成Cypherが Opportunity.name のみで狭い(NG)
  • D4: diagnostics mirror はOK(OK)

Comments

コメントを残す

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