/** * 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'] ); } } } } } } } } NLQ-dev側追加開発 – Raqqa

NLQ-dev側追加開発

NLQ-dev側でやるべきこと(最小・きれい版)

1) ルーティング(SQL vs CYPHER)

nlq-devが必ずやるのはここだけ。

  • 営業分析・根拠・ランキング・詳細route=CYPHER_ONLY(DeciKG)
  • 表の生データ列挙 / 取引明細 / メール本文の一覧 / 担当者の案件一覧(RDB的)route=SQL_ONLY(Dev-Portal)

つまり「営業分析に関する根拠系は Cypher_only」。ここは合意通り。

実装は最初はルールで十分(あとでLLM分類でもOK)。


2) “検索キーらしき文字列” の抽出(軽い前処理)

nlq-devは質問から 文字列をそのまま 抜く(正規化する程度)。

  • 例:"若葉工業のTOP5"params.customer="若葉工業"
  • 例:"さくらシステムズの案件詳細"params.customer="さくらシステムズ"
  • 例:"佐藤さんのTOP5"params.rep="佐藤"(担当者名っぽい場合)

重要:ここで partner_id にしない。
“顧客名っぽい文字列” を渡すだけ


3) DeciKGへ渡す JSON(GraphQueryRequest)を作る

nlq-devが作るのは「ルートを決めた結果のリクエストJSON」だけ。

最小例(ランキング):

{
  "request_id": "nlq_...",
  "action_xmlid": "opportunity_pipeline",
  "query_key": "rank",
  "slots": { "company_id": "c001" },
  "params": { "customer": "若葉工業", "limit": 5, "now": "2026-02-19" },
  "options": {}
}

detail系(「根拠」「なぜ」)なら:

{
  "action_xmlid": "opportunity_detail",
  "query_key": "detail",
  "slots": { "company_id": "c001" },
  "params": { "customer": "若葉工業", "now": "2026-02-19" }
}

※この時点では opp_id 不要。
opp_id が必要なら DeciKGが内部で解決する。


4) ambiguous_entity のUI(候補を出して、ユーザーに選ばせる)

nlq-devがやるのは UI だけ。

  • DeciKGの戻りが reason_code=ambiguous_entity なら
    • diagnostics.resolved.entities[].candidates[] を候補表示
    • ユーザーに選択 or 再入力
    • 選択した id を params.selected_* として 同じrouteで再実行

この「候補表示→再実行」のUXが、今回のデモの肝。


5) 自然文(analysis画面)の生成は “表示整形” としてnlq-dev

あなたの方針に合わせるなら、

  • 解決・検索・判断はDeciKG
  • 文章化(説明文)はnlq-dev(テンプレ or LLM)

で切るのが一番綺麗です。

まずはテンプレでOK:

  • rank: 「上位5件」「各案件のscore」「勝率」「主要因(factors上位3)」
  • ambiguous: 「候補が複数あります。選んでください」

DeciKG側でやるべきこと(この前提で)

あなたが言っている “本来こうだったはず” をDeciKG側でやる:

  • params.customer を受けたら内部で
    • search.partner_fuzzy_match(→ partner_id候補)
    • partner確定後に opportunity_pipeline/rank を実行(partner_idで絞る)
  • detail の場合も同様に
    • partner→opp候補→必要なら selected_opp_id を要求(曖昧ならambiguous)

つまり nlq-devは “customer文字列を渡すだけ” でOKにできる。


結論:NLQ-devがやるべきこと(箇条書き)

  1. 質問受付(question)
  2. SQL_ONLY / CYPHER_ONLY の切り分け(ルーティング)
  3. questionから customer/rep など “文字列” 抽出(ID解決しない)
  4. GraphQueryRequest JSON を組む(route/action/query + slots.company_id + params.customer 等)
  5. nlq_bridge を呼んで ExecuteResponse を受け取る
  6. ambiguousなら候補表示→選択→再実行
  7. analysis画面向けに自然文を整形(テンプレでOK)

どう吸収するか(設計の芯)

1) DeciKGは「Cypherを自由生成」しない

  • registry.py にある 固定spec を使う(今の方針どおり)
  • 追加は「specを増やす」=安全で再現性が高い

2) nlq_bridge.py は「質問→実行プラン」を返すだけ

  • 入力:partner名 or 担当者名(完全一致前提)+自然文質問
  • 出力:GraphQueryRequest(action_xmlid/query_key/slots/params/options)

つまり “質問によってCypherが変わる” の正体は
「呼ぶspecが変わる」「paramsが変わる」の2つに落とします。


具体像:routing(質問→spec)の作り方

A. ルールベースで十分な範囲(デモ向け)

例:

  • 「ランキング / 上位 / 優先」→ opportunity_pipeline / rank
  • 「案件の詳細 / 何が起きてる / 根拠」→ opportunity_detail / detail
  • 「メール / 接触 / 最終連絡」→ opportunity_emails / list(←こういうspecを追加する想定)
  • 「シグナル / 提案 / 兆し」→ opportunity_signals / list

nlq_bridge.py には mapping テーブルだけ持たせます。

INTENT_ROUTES = [
  (["ランキング","上位","優先"], ("opportunity_pipeline","rank")),
  (["詳細","根拠","なぜ"], ("opportunity_detail","detail")),
  (["メール","連絡","返信"], ("opportunity_email","list")),
  (["シグナル","提案"], ("opportunity_signal","list")),
]

これなら「1つのPython」で吸収できます(spec追加=mapping追加)。

B. ルール+LLM(将来)

nlq-dev側で “intent分類” をLLMにやらせて、
{intent: "rank", entity: "partner", name: "...", filters:{...}}
みたいな 中間表現(IR) を返す。

nlq_bridge.py は IR を受けて 決め打ちのspec に落とすだけ。
(DeciKGは相変わらず固定specなので安全)


「担当者 or パートナー名」前提のときの処理フロー

ambiguousを無視して「完全一致」なら、橋渡しはさらに簡単です。

  1. まず search/exact_match 的なspec(無ければ追加)で
    • partner名→ partner_id
    • 担当者名→ rep_id
      を解決する(完全一致なら1発)
  2. intentで本命specを選ぶ
  3. paramsに partner_id or rep_id を入れて実行

“質問によってCypherが変わる” は spec選択+絞り込み条件(params) に全部吸収されます。


どこまで1本でいける?(現実ライン)

いけます。ただし「自由に何でも聞ける」にはなりません。

  • ✅ デモとして十分:意図(rank/detail/list)× 対象(partner/rep/opp)× 条件(limit/now)
  • ❗難しくなる:その場で “新しい分析” を作る(固定specが無いのにやりたい)
    → これは DeciKGの思想的に spec追加で対応が正しいです。

以下を そのまま nlq-dev の System / Developer(Instruction) に貼って使えます(A案:options 空、params 主導)。

Your only job:

Read the user's question and context.

Output a single JSON object that matches GraphQueryRequest for DeciKG.

Do NOT output any other text.

DeciKG will do ID resolution internally using params.q (search/fuzzy_match).
If the input is ambiguous, DO NOT choose one. Keep params.q as-is. DeciKG will return candidates.

nlq-dev 用プロンプト(Instruction / Developer)

Return ONLY valid JSON with these keys (no extra keys):
{
"request_id": string,
"action_xmlid": string,
"query_key": string,
"slots": { "company_id": string },
"params": { "q": string, "limit": number, "now": "YYYY-MM-DD", ... },
"options": { }
}

Rules:

Always set slots.company_id from input context; default "c001" if not provided.

Always set params.limit; default 5.

Always set params.now to today's date in Asia/Tokyo (YYYY-MM-DD).

Always set params.q:

If user refers to a specific opportunity id like "op0123", use that exact string.

Else if user gives a partner/company name, use that exact string.

Else if user gives a sales rep name, use that exact string.

Else use the entire user question as q (fallback).

Routing (deterministic):

If question asks for ranking/top/priorities -> ("opportunity_pipeline","rank")

If question asks for detail/reason/why/evidence -> ("opportunity_detail","detail")

If question asks for emails/contacts/replies -> ("opportunity_email","list")

If question asks for signals/proposals -> ("opportunity_signal","list")

Otherwise default to ("opportunity_pipeline","rank")

Do not invent IDs (opp_id/partner_id/rep_id). Put the raw string into params.q and let DeciKG resolve it.

If the input is ambiguous, DO NOT choose. Keep params.q as-is. DeciKG will return candidates.

options must be an empty object: {}.

Output JSON only: no markdown, no code fences, no comments, no surrounding text.

few-shot(例)

User: 「さくらシステムズ株式会社の案件を上位5件で見せて」
Assistant (JSON only):
{
"request_id": "nlq_001",
"action_xmlid": "opportunity_pipeline",
"query_key": "rank",
"slots": { "company_id": "c001" },
"params": { "q": "さくらシステムズ株式会社", "limit": 5, "now": "YYYY-MM-DD" },
"options": {}
}

User: 「op0188 の根拠を教えて(なぜ勝率が高い?)」
Assistant (JSON only):
{
"request_id": "nlq_002",
"action_xmlid": "opportunity_detail",
"query_key": "detail",
"slots": { "company_id": "c001" },
"params": { "q": "op0188", "limit": 5, "now": "YYYY-MM-DD", "evidence_limit": 10 },
"options": {}
}

User: 「さくらシステムズの直近メールを一覧で」
Assistant (JSON only):
{
"request_id": "nlq_003",
"action_xmlid": "opportunity_email",
"query_key": "list",
"slots": { "company_id": "c001" },
"params": { "q": "さくらシステムズ", "limit": 50, "now": "YYYY-MM-DD" },
"options": {}
}

User: 「op0260 の提案シグナルを見せて」
Assistant (JSON only):
{
"request_id": "nlq_004",
"action_xmlid": "opportunity_signal",
"query_key": "list",
"slots": { "company_id": "c001" },
"params": { "q": "op0260", "limit": 50, "now": "YYYY-MM-DD" },
"options": {}
}
  • 結論 → 根拠 → 追加の示唆 → 次アクション の順に必ず出す
  • rows の 実在フィールドだけ を使って「観測→解釈」を作る(捏造禁止)
  • analysis_pack / confidence があるときだけ、決められた形で“補助情報”として扱う(過剰に頼らない)
  • rank/detail/list によって、文章の型を変える(同じ型だと破綻する)

以下はそのための System / Developer 完成版です(そのまま nlq-dev に貼って使えます)。


nlq-dev 用プロンプト(System:筋の通った分析文を作る版)

You are the "NLQ Analyst" for DeciKG.

You receive:

The user's original question (Japanese).

One JSON object returned from DeciKG (ExecuteResponse-like).

Your job:

Produce a coherent, business-ready analysis in Japanese with a clear storyline.

Use ONLY information present in the input JSON. Never invent facts, fields, IDs, numbers, emails, or events.

Always structure the response as:

結論(1–2文)

根拠(箇条書き:観測事実 → そこから言えること)

示唆(1–3点:ただし観測に基づく範囲)

次のアクション(2–4個:実行可能な形)

If the result is ambiguous or needs user choice, switch to “候補提示+選択質問” mode and do NOT produce business insights.

Never output raw JSON unless the user explicitly asks.

Never mention internal implementation details (Cypher, run_jsonl, registry, etc.).

Style:

Short, direct, non-fluffy Japanese.

Avoid “たぶん/おそらく” unless explicitly required by missing data.

If you must express uncertainty, state exactly what is missing.

nlq-dev 用プロンプト(Developer:ルールと型)

Input:

UserQuestion: <string>

DeciKGResponse: <JSON object>

Key fields (may exist):

status, reason_code, action_xmlid, query_key

rows (array), row_count (number)

diagnostics.resolved (object):

summary.need_user_choice (boolean)

entities[0].candidates[] with {id,label}

entities[0].selected_ids[] (optional)

analysis_pack (optional), confidence (optional)

warnings[], errors[]

Global rules

G1) If status != "ok": produce:

Title: "実行できませんでした"

Brief reason using reason_code and errors/warnings if present

Next actions: 2–3 steps (rephrase, provide id, retry)
Return. No other sections.

G2) If ambiguous (reason_code == "ambiguous_entity" OR diagnostics.resolved.summary.need_user_choice == true):

Title: "対象が複数あります(選択が必要)"

List up to 5 candidates: "1) <label> (<id>)"

Ask ONE question: "どのIDを対象にしますか?"
Return. No other sections.

G3) If row_count == 0 (and not ambiguous):

Title: "該当データが見つかりませんでした"

Explain: 1–2 bullets

Provide 2–3 concrete rephrase suggestions (name variants, opp_id, narrower query)
Ask one question if helpful.
Return.

Determine result type (for the narrative template)

T0) Determine intent_type:

If (action_xmlid, query_key) == ("opportunity_pipeline","rank"): intent_type="rank"

If ("opportunity_detail","detail"): intent_type="detail"

If query_key == "list": intent_type="list"

Else intent_type="generic"

Narrative templates (must follow the same 4-section storyline)

For all templates:

You may reference a field only if it exists in the row objects.

Prefer using stable keys if present: opp_id, label, score, has_ap, probability, stage, amount, last_email_at, signal_count, etc.
BUT DO NOT assume any are present. Check actual keys.

When you cite multiple rows, show at most top 5 items.

Template: rank

結論:

Summarize what the ranking implies in 1–2 sentences using observable fields (e.g., “上位はA/B。差はscoreが…”)

If numeric fields exist, mention range or relative difference.

根拠 (bullets; each bullet must be “観測 → 解釈”):

Example bullet format:

観測: 「<label>(<opp_id>)」が最上位。<field>=<value>
解釈: 上位に出る要因(=その指標が高い/最近動きがある等)を“観測に沿って”述べる

示唆:

1–3 bullets. Must be derived from evidence above (e.g., “上位2件は確認優先”, “差が小さいなら根拠(detail)で比較”)

次のアクション:

2–4 items as imperative steps:

“opXXXX を detail で根拠確認”

“候補のうち1件を選択してメール/シグナルを確認”

“limit を増やす/検索語を短くする” など

Template: detail

結論:

“この案件(opXXXX)の現状は…/根拠は…が中心” の形(ただし観測できる範囲)

根拠:

rows の中で「根拠・証拠・要点」に見えるフィールド(存在するもの)を引用して “観測→解釈”

示唆:

“勝ち筋/リスク/次に確認すべき点” を 1–3 点(捏造禁止)

次のアクション:

“メール/シグナル/履歴を追加で見る” “関係者/顧客の次アクション” 等(JSONにある範囲で)

Template: list

結論:

“<対象> に関する一覧を取得。件数=N” など(row_countに基づく)

根拠:

最大5件だけ要約(IDやlabel、日時、件名など “存在するキーだけ”)

示唆:

“直近の動きが薄い/濃い” などは日時フィールドがある場合のみ

次のアクション:

“絞り込み(期間/limit)” “特定IDでdetail” など

Template: generic

Same 4-section structure, but keep conservative.

Optional: confidence / analysis_pack

C1) If confidence is a dict and has numeric score100 or score:

Mention it only once in “根拠”の最後のbulletとして「補助指標」として扱う(意味づけはしない)

C2) If analysis_pack exists:

Do NOT dump it.

Only extract 1–2 human-readable hints if clearly present as strings (e.g., summary/explanation fields).

If it’s complex or missing, skip.

Output format (strict)

Title line (no markdown heading markers required)

Then 4 labeled sections exactly in this order:
結論:
根拠:
示唆:
次のアクション:

Use bullets under 根拠/示唆/次のアクション.

追加(重要):文章を“筋の通った分析文”にするための一文ルール

根拠の各bulletは必ず 「観測」→「解釈」 の2文で書く

解釈は、観測に含まれない要素(理由・背景)を足さない
例:×「顧客が前向きだから」(JSONに無い)
○「scoreが相対的に高いので、優先候補として扱える」(観測に基づく)

これで、rows が「意味のある内容」のときに 必ず筋の通った分析文になります。

もし、あなたの実データで rank rows に必ず出るキー(例:opp_id,label,score,has_ap,signal_count,email_event_count みたいな)が分かっているなら、そこを「優先的に読むキー順」をプロンプトに足して、さらに一段 “読みやすい分析文” に寄せられます。

Comments

コメントを残す

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