/** * 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'] ); } } } } } } } } 「業務知識グラフ(Enterprise Knowledge Graph)」の正しい育て方 – Raqqa

「業務知識グラフ(Enterprise Knowledge Graph)」の正しい育て方

最初から企業別に深く入りすぎず、共通構造+軽い結合で“基盤モデル”を作っておき、後から企業別に強化する。これが「スケールとカスタム」の両立策です。


🧩 1. 薄い共通モデル(Foundation Layer)

まずはすべての企業で共通する業務ドメインの骨格を小さく定義します。
(Neo4j / Postgres どちらでも格納できます)

カテゴリノード例主な関係目的
人・組織Person, Org, Department(:Person)-[:BELONGS_TO]->(:Org)誰が誰に関係するか
プロセスProject, Order, Invoice, BankReconciliation(:Invoice)-[:SETTLED_BY]->(:BankReconciliation)会計・取引の流れ
製品・部品Product, Component, BOM(:Product)-[:HAS_PART]->(:Component)製造・構成要素
顧客接点Email, ChatMessage, Meeting(:Person)-[:SENT]->(:Email)非構造情報の入り口
意味構造Decision, ActionItem, Topic(:Email)-[:MENTIONS]->(:Topic)暗黙知の整理軸

ここで大事なのは、「すべてのメール・チャット・Odooデータが、
どれか1つの“共通ノード”にゆるく繋がっている」ことです。

例えば:

(:Email)-[:ABOUT]->(:Invoice)
(:ChatMessage)-[:ABOUT]->(:Project)
(:Email)-[:MENTIONS]->(:BankReconciliation)

これだけで「誰が、どの請求・入金・案件の話をしているか」が
横断的に見えるようになります。


⚙️ 2. 実装の段階(3層アーキテクチャ)

目的具体例
L0: データ接着層(Universal Connectors)各システムの基本項目を取り出すGmail, Slack, Odoo (sales, invoice, stock)
L1: 意味付け層(Semantic Bridge)メール文・件名・添付などをEmbedding+LLM解析「この文は入金確認を指す」などの分類
L2: 構造化層(Knowledge Graph)ノード化し、関連リンクを張る(:Email)-[:ABOUT]->(:BankReconciliation)

最初は L0+L1 のみで“関係候補”を推測 → Neo4j へ反映して L2 を作ります。


💡 3. 「薄いモデル」でできること(すぐ価値が出る)

機能実現方法
類似メール検索ChromaDBで埋め込み検索
メール→取引照合LLMで「入金」「送金」「残高一致」などを分類
会計仕訳リンクOdooのaccount.moveまたはbank.statement.lineへ軽い参照リンクを付与
暗黙知抽出「どんな取引でトラブルが多いか」「顧客からの指摘が多い工程」などをNeo4jの関係数で可視化
ドメイン拡張企業別にノード属性やリレーションを追加

🧠 4. 「基盤モデル」を作る際のコツ

  1. 全社共通のオントロジー名を決める(例:Invoice, Payment, Project, Person
  2. ゆるい関係で繋ぐABOUT, MENTIONS, REFERS_TO など曖昧でも可)
  3. LLM分類器を1本用意して「このテキストはどの業務カテゴリか」を自動タグ付け # 例: promptで分類 classify_prompt = """ Classify the text into one of: [Invoice, Payment, Order, Delivery, Reconciliation, Other]. Respond with JSON: {"category": "<label>"} """
  4. Neo4jでは共通インデックスを固定化 CREATE INDEX idx_category IF NOT EXISTS FOR (n) ON (n.category); CREATE INDEX idx_external_id IF NOT EXISTS FOR (n) ON (n.external_id);
  5. 後で強化学習できるようにEvidenceを残す
    (どのメールから、どの取引推論が生まれたか)

🪴 5. 成長パス(プロジェクト計画イメージ)

フェーズ目的期間目安
Phase 1: 基盤構築薄い共通ノード+Chroma検索連携1–2か月
Phase 2: 暗黙知抽出 PoCメール・チャットから「業務トピック」分類(決済・調達・遅延)1か月
Phase 3: Odoo連携Odooのモデル(invoice, stock, crm)にexternal_idリンク1か月
Phase 4: 企業別強化各社固有ノード・関係の追加、辞書拡張継続

🚀 まとめ

  • まずは **共通業務領域の「薄い知識グラフ」**を全企業で使える形にする。
  • メール/チャット → 埋め込み+軽分類器 → Odoo参照リンク → Neo4j格納 という流れで、企業を問わず動く。
  • 後からヒアリングで各社固有ノード(例:承認フロー・特定部品群)を追加すれば“深い暗黙知”に進化する。
  • これができれば「企業共通の業務知識OS」のようなサービスが成立します。

まずは“薄い共通モデル(基盤層)”の雛形を一式置いておきます。
そのまま PoC に使えるように、**(1) ノード/リレーション一覧(約10種) → (2) Cypher DDL(制約/索引付き) → (3) LLM分類プロンプト(英日対応)**の順にまとめました。


1) ノード/リレーション一覧(基盤レイヤ)

ノード(代表プロパティ)

  • Person: { email_hash, name?, org_key?, role?, tenant }
  • Org: { org_key, legal_name?, domain?, tenant }
  • Department: { dept_key, name, org_key, tenant }
  • Project: { project_key, name?, customer_org_key?, tenant }
  • Order(Sales/Purchase 両用): { order_key, order_type: 'sales'|'purchase', partner_org_key, order_date, tenant }
  • Invoice: { invoice_key, order_key?, partner_org_key, issue_date, due_date?, currency?, amount?, tenant }
  • BankReconciliation(入出金照合の核): { recon_key, statement_date, method?, tenant }
  • Payment: { payment_key, bank_account?, paid_on, amount?, currency?, tenant }
  • Product: { product_key, name?, sku?, tenant }
  • Component: { component_key, name?, tenant }
  • Email(Evidence の役割): { message_id, sent_at, subject?, snippet?, from_hash, to_hashes[], thread_id?, lang?, tenant }
  • ChatMessage(Evidence): { message_id, sent_at, text?, user_hash, channel?, thread_ts?, lang?, tenant }
  • Topic(任意・タグ軸): { name, tenant }

external_id を Odoo 由来で持つなら、*_keyodoo:account.move/123 のようにネームスペース付きを推奨。

リレーション(代表)

  • (:Person)-[:BELONGS_TO]->(:Org)
  • (:Person)-[:IN_DEPT]->(:Department)
  • (:Person)-[:SENT]->(:Email|:ChatMessage)
  • (:Email|:ChatMessage)-[:ABOUT]->(:Project|:Order|:Invoice|:Payment|:BankReconciliation|:Product)
  • (:Email|:ChatMessage)-[:MENTIONS]->(:Topic)
  • (:Email)-[:IN_THREAD]->(:Email)(親子/スレッド)
  • (:ChatMessage)-[:IN_THREAD]->(:ChatMessage)
  • (:Invoice)-[:FOR_ORDER]->(:Order)
  • (:Payment)-[:SETTLES]->(:Invoice)(支払→請求のひも付け)
  • (:BankReconciliation)-[:SETTLED_BY]->(:Payment)(照合作業→個別支払)
  • (:Product)-[:HAS_PART]->(:Component)
  • (:Project)-[:FOR_CUSTOMER]->(:Org)

まずは ABOUTSETTLES / FOR_ORDER / SETTLED_BY を“薄く”張るだけで「メール・チャット ↔ 会計/受発注/製品」を横断できます。


2) Cypher DDL(制約 / 索引 ひな形)

// ===== Tenancy(任意) =====
CREATE CONSTRAINT tenant_org IF NOT EXISTS
FOR (o:Org) REQUIRE (o.org_key, o.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_person IF NOT EXISTS
FOR (p:Person) REQUIRE (p.email_hash, p.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_dept IF NOT EXISTS
FOR (d:Department) REQUIRE (d.dept_key, d.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_project IF NOT EXISTS
FOR (p:Project) REQUIRE (p.project_key, p.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_order IF NOT EXISTS
FOR (o:Order) REQUIRE (o.order_key, o.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_invoice IF NOT EXISTS
FOR (i:Invoice) REQUIRE (i.invoice_key, i.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_payment IF NOT EXISTS
FOR (p:Payment) REQUIRE (p.payment_key, p.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_recon IF NOT EXISTS
FOR (r:BankReconciliation) REQUIRE (r.recon_key, r.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_product IF NOT EXISTS
FOR (p:Product) REQUIRE (p.product_key, p.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_component IF NOT EXISTS
FOR (c:Component) REQUIRE (c.component_key, c.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_email IF NOT EXISTS
FOR (e:Email) REQUIRE (e.message_id, e.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_chat IF NOT EXISTS
FOR (m:ChatMessage) REQUIRE (m.message_id, m.tenant) IS UNIQUE;

CREATE CONSTRAINT tenant_topic IF NOT EXISTS
FOR (t:Topic) REQUIRE (t.name, t.tenant) IS UNIQUE;

// ===== よく使う索引 =====
CREATE INDEX idx_person_org IF NOT EXISTS FOR (p:Person) ON (p.org_key);
CREATE INDEX idx_person_dept IF NOT EXISTS FOR (p:Person) ON (p.tenant, p.org_key);

CREATE INDEX idx_project_customer IF NOT EXISTS FOR (p:Project) ON (p.customer_org_key);
CREATE INDEX idx_order_date IF NOT EXISTS FOR (o:Order) ON (o.order_date);
CREATE INDEX idx_invoice_dates IF NOT EXISTS FOR (i:Invoice) ON (i.issue_date, i.due_date);
CREATE INDEX idx_payment_date IF NOT EXISTS FOR (p:Payment) ON (p.paid_on);

CREATE INDEX idx_email_sent IF NOT EXISTS FOR (e:Email) ON (e.sent_at);
CREATE INDEX idx_chat_sent IF NOT EXISTS FOR (m:ChatMessage) ON (m.sent_at);

CREATE INDEX idx_about_lookup IF NOT EXISTS FOR (t:Topic) ON (t.name);

// ===== 参照整合(任意:弱制約代替の運用) =====
// 例えば ABOUT の先に必ず tenant が一致するよう、アプリ側でチェックするのが無難。

// ===== (任意)ベクトル検索を使う場合の索引 =====
// m.embedding / e.embedding に LIST<FLOAT> を格納する前提
// 次元数は利用モデルに合わせて(例:1536)
CREATE VECTOR INDEX email_embed_idx IF NOT EXISTS
FOR (e:Email) ON (e.embedding)
OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' }};

CREATE VECTOR INDEX chat_embed_idx IF NOT EXISTS
FOR (m:ChatMessage) ON (m.embedding)
OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' }};

まずは ユニーク制約(tenant+自然キー)日時索引 だけで十分です。
ベクトル索引は「後から」でも OK(PoC 段階では Chroma 側で近傍探索→ID 連携でも回せます)。


3) LLM分類プロンプト(英日対応・JSON厳格)

3.1 出力スキーマ(固定)

  • category(必須・1つ):
    ["Invoice","Payment","Order","Delivery","Reconciliation","Project","Product","Support","Scheduling","Other"]
  • about_refs(0..n・軽い参照):例)["invoice:INV-2025-0012","order:SO-000345","project:PRJ-MAPS"]
  • intent(任意・1つ):["inform","request","approval","issue","schedule","other"]
  • signals(任意・0..n):["due_date","amount","bank","discrepancy","delay"]
  • confidence(0.0–1.0)
  • language(”ja” / “en” / 他)

JSON 例(英語メール)

{
  "category": "Reconciliation",
  "about_refs": ["invoice:INV-2025-0012","payment:PAY-83421"],
  "intent": "inform",
  "signals": ["discrepancy","amount","bank"],
  "confidence": 0.86,
  "language": "en"
}

3.2 英語プロンプト(Classifier / JSONモード想定)

SYSTEM:
You are a strict business email/chat classifier for ERP workflows. 
Return ONLY a single-line JSON object following the schema provided.
No prose, no explanations.

USER:
Classify the following text into business category and attributes.

Categories (exactly one):
["Invoice","Payment","Order","Delivery","Reconciliation","Project","Product","Support","Scheduling","Other"]

Intent (0..1):
["inform","request","approval","issue","schedule","other"]

Signals (0..n):
["due_date","amount","bank","discrepancy","delay"]

Output JSON fields:
{
  "category": "<one_of_categories>",
  "about_refs": ["<light_reference_or_empty>"],   // e.g., "invoice:INV-2025-0012"
  "intent": "<optional_intent_or_omit>",
  "signals": ["<zero_or_more_signals>"],
  "confidence": <0.0-1.0>,
  "language": "<ja|en|...>"
}

Text:
---
{TEXT}
---

Rules:
- Be conservative: if unsure between Invoice/Payment/Reconciliation, prefer "Reconciliation" only when the message is explicitly about matching statements or linking invoices to payments.
- Prefer extracting light references from obvious tokens (invoice/order numbers, project codes).
- Respond with exactly one compact JSON line. No comments.

3.3 日本語プロンプト(同等内容)

SYSTEM:
あなたはERP業務向けの厳格なメール/チャット分類器です。
出力は指定のJSONオブジェクト1行のみ。説明文は不要です。

USER:
以下の本文を分類し、指定のスキーマでJSONを返してください。

カテゴリ(いずれか1つ):
["Invoice","Payment","Order","Delivery","Reconciliation","Project","Product","Support","Scheduling","Other"]

インテント(0〜1個):
["inform","request","approval","issue","schedule","other"]

シグナル(0個以上):
["due_date","amount","bank","discrepancy","delay"]

出力JSONの形式:
{
  "category": "<上記のいずれか1つ>",
  "about_refs": ["<簡易参照 or 空>"],   // 例: "invoice:INV-2025-0012"
  "intent": "<任意: 上記のいずれか or 省略>",
  "signals": ["<0個以上>"],
  "confidence": <0.0-1.0>,
  "language": "<ja|en|...>"
}

本文:
---
{TEXT}
---

ルール:
- Invoice/Payment/Reconciliation の迷いがある場合、「入出金の照合(明細と請求の突合せ)」が主題なら Reconciliation を選ぶ。
- 請求書番号や注文番号、案件コードなど明白なトークンから about_refs を素直に抽出。
- 出力は厳密に1行のJSONのみ。コメントや説明は不要。

3.4(任意)検出の精度を上げるための補助プロンプト

  • 参照抽出ヘルパ(番号・金額・日付・取引先名の抽出だけを行う小プロンプト)
  • 正規化辞書(Odoo 側の external_id や命名規則を few-shot で提示)
  • 英⇄日 並列表現(請求書=Invoice, 照合=Reconciliation, 入金=Payment などの対訳表)

使い始めの運用メモ(超要点)

  • 最初は Email/Chat → category+about_refs の“薄い”付与だけで十分。
  • Reconciliation 検出は signals=[“discrepancy”,”amount”,”bank”] の組合せが効きます。
  • 企業別の細かいルールは後から Topic でタグ拡張 or 別モデル(Issue/Decision)として増やす方が安全。

Comments

コメントを残す

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