-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ui): show business context on related-node chips (v0.75.0) #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
94d86ea
9ff7ad1
1b00bf4
7c7d0d9
6c29b2f
5eeaa7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Mapping | ||
| from typing import Any | ||
| from uuid import UUID | ||
|
|
||
|
|
@@ -275,6 +276,55 @@ async def load_visible_subgraph( | |
| return [edge_spec_from_row(row) for row in rows] | ||
|
|
||
|
|
||
| def compact_affiliation_display_names( | ||
| rows: list[Mapping[str, Any]], | ||
| ) -> dict[str, str]: | ||
| """Return at most one display organization per person. | ||
|
|
||
| A resolved ``corporate_entity`` is one identity, labeled with | ||
| ``catalog_entity_name`` (falling back to the raw extraction | ||
| string). Unresolved names that casefold-match that catalog label | ||
| collapse into it -- the catalog name wins. Distinct unresolved | ||
| names stay distinct. A person with more than one remaining | ||
| identity is omitted so the chip never invents a primary org. | ||
| """ | ||
| catalog_ids: dict[str, set[str]] = {} | ||
| catalog_labels: dict[str, dict[str, str]] = {} | ||
| unresolved_names: dict[str, set[str]] = {} | ||
| for row in rows: | ||
| person_id = str(row["person_id"]) | ||
| raw_name = (row["affiliated_organization_name"] or "").strip() | ||
| catalog_id = row["affiliated_corporate_entity_id"] | ||
| catalog_name = (row["catalog_entity_name"] or "").strip() | ||
| if catalog_id is not None: | ||
| identity = str(catalog_id) | ||
| catalog_ids.setdefault(person_id, set()).add(identity) | ||
| label = catalog_name or raw_name | ||
| if label: | ||
| catalog_labels.setdefault(person_id, {})[identity] = label | ||
| continue | ||
| if raw_name: | ||
| unresolved_names.setdefault(person_id, set()).add(raw_name) | ||
|
|
||
| display_names: dict[str, str] = {} | ||
| for person_id in set(catalog_ids) | set(unresolved_names): | ||
| labels_by_id = catalog_labels.get(person_id, {}) | ||
| catalog_name_fold = {name.casefold() for name in labels_by_id.values()} | ||
| leftover_names = { | ||
| name | ||
| for name in unresolved_names.get(person_id, set()) | ||
| if name.casefold() not in catalog_name_fold | ||
| } | ||
| identity_count = len(catalog_ids.get(person_id, set())) + len(leftover_names) | ||
| if identity_count != 1: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| continue | ||
| if leftover_names: | ||
| display_names[person_id] = next(iter(leftover_names)) | ||
| elif labels_by_id: | ||
| display_names[person_id] = next(iter(labels_by_id.values())) | ||
| return display_names | ||
|
|
||
|
|
||
| async def hydrate_related_nodes( | ||
| conn: asyncpg.Connection, | ||
| related: list[tuple[str, float]], | ||
|
|
@@ -283,6 +333,11 @@ async def hydrate_related_nodes( | |
|
|
||
| Unknown ids are dropped. Ontology fields are omitted (not faked) | ||
| when ``node_type_code`` has no term in lineageweave-kg.ttl. | ||
| Person nodes carry compact affiliation context only when exactly one | ||
| distinct organization identity is known. A resolved catalog org | ||
| supplies ``entity_name``; aliases of that same org collapse into it. | ||
| Multiple distinct affiliations are omitted rather than collapsed | ||
| into an invented primary organization. | ||
| """ | ||
| person_ids: list[str] = [] | ||
| post_ids: list[str] = [] | ||
|
|
@@ -305,6 +360,22 @@ async def hydrate_related_nodes( | |
| person_ids, | ||
| ) | ||
| } if person_ids else {} | ||
| affiliations = compact_affiliation_display_names( | ||
| await conn.fetch( | ||
| """ | ||
| select | ||
| pa.person_id, | ||
| pa.affiliated_organization_name, | ||
| pa.affiliated_corporate_entity_id, | ||
| ce.entity_name as catalog_entity_name | ||
| from person_affiliation pa | ||
| left join corporate_entity ce | ||
| on ce.corporate_entity_id = pa.affiliated_corporate_entity_id | ||
| where pa.person_id = any($1::uuid[]) | ||
| """, | ||
| person_ids, | ||
| ) | ||
| ) if person_ids else {} | ||
| posts = { | ||
| str(row["post_id"]): row | ||
| for row in await conn.fetch( | ||
|
|
@@ -315,11 +386,19 @@ async def hydrate_related_nodes( | |
| corps = { | ||
| str(row["corporate_entity_id"]): row | ||
| for row in await conn.fetch( | ||
| "select corporate_entity_id, entity_name from corporate_entity where corporate_entity_id = any($1::uuid[])", | ||
| "select corporate_entity_id, entity_name, entity_level_code " | ||
| "from corporate_entity where corporate_entity_id = any($1::uuid[])", | ||
| corp_ids, | ||
| ) | ||
| } if corp_ids else {} | ||
|
|
||
| side_labels = await labels_for_codes( | ||
| conn, [row["person_side_code"] for row in people.values()] | ||
| ) | ||
| level_labels = await labels_for_codes( | ||
| conn, [row["entity_level_code"] for row in corps.values()] | ||
| ) | ||
|
|
||
| payload: list[dict[str, Any]] = [] | ||
| for node_type_code, node_id, score in parsed: | ||
| item: dict[str, Any] = { | ||
|
|
@@ -329,12 +408,20 @@ async def hydrate_related_nodes( | |
| **ontology_annotations(node_type_code), | ||
| } | ||
| if node_type_code == NODE_PERSON and node_id in people: | ||
| side = people[node_id]["person_side_code"] | ||
| item["label"] = people[node_id]["person_name"] | ||
| item["person_side_code"] = people[node_id]["person_side_code"] | ||
| item["person_side_code"] = side | ||
| item["person_side_label"] = side_labels.get(side, side) | ||
| org = affiliations.get(node_id) | ||
| if org: | ||
| item["affiliation_organization_name"] = org | ||
| elif node_type_code == NODE_POST and node_id in posts: | ||
| item["label"] = posts[node_id]["post_title"] | ||
| elif node_type_code == NODE_CORPORATE_ENTITY and node_id in corps: | ||
| level = corps[node_id]["entity_level_code"] | ||
| item["label"] = corps[node_id]["entity_name"] | ||
| item["entity_level_code"] = level | ||
| item["entity_level_label"] = level_labels.get(level, level) | ||
| else: | ||
| continue | ||
| payload.append(item) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This head also bumps
pyproject.toml/lineageweave/__init__.py/frontend/package.jsonto 0.75.0. Tagging 0.75.0 from5eeaa7fwould ship the orchestrator-mode lock while the notes still call it Unreleased. Move this bullet under[0.75.0], or drop the version bump. #123 folds it.