hyperagents 1.2.3rc5.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge linker for establishing relationships between civilization nodes.
|
|
3
|
+
Supports automatic link detection, relationship inference, and graph traversal.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import UUID, uuid4
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from ..schema import (
|
|
14
|
+
CivilizationNode, NodeType,
|
|
15
|
+
SOP, Role, Person, Checklist, Runbook, Workflow, JobDescription,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LinkType(str, Enum):
|
|
22
|
+
# Hierarchical relationships
|
|
23
|
+
PARENT_OF = "parent_of"
|
|
24
|
+
CHILD_OF = "child_of"
|
|
25
|
+
|
|
26
|
+
# Process relationships
|
|
27
|
+
PRECEDES = "precedes"
|
|
28
|
+
FOLLOWS = "follows"
|
|
29
|
+
TRIGGERS = "triggers"
|
|
30
|
+
TRIGGERED_BY = "triggered_by"
|
|
31
|
+
|
|
32
|
+
# Ownership/responsibility
|
|
33
|
+
OWNS = "owns"
|
|
34
|
+
OWNED_BY = "owned_by"
|
|
35
|
+
RESPONSIBLE_FOR = "responsible_for"
|
|
36
|
+
ASSIGNED_TO = "assigned_to"
|
|
37
|
+
|
|
38
|
+
# Reference relationships
|
|
39
|
+
REFERENCES = "references"
|
|
40
|
+
REFERENCED_BY = "referenced_by"
|
|
41
|
+
RELATED_TO = "related_to"
|
|
42
|
+
|
|
43
|
+
# Role relationships
|
|
44
|
+
REQUIRES_ROLE = "requires_role"
|
|
45
|
+
REQUIRED_BY_ROLE = "required_by_role"
|
|
46
|
+
USES_TOOL = "uses_tool"
|
|
47
|
+
|
|
48
|
+
# Version relationships
|
|
49
|
+
VERSION_OF = "version_of"
|
|
50
|
+
SUPERSEDES = "supersedes"
|
|
51
|
+
SUPERSEDED_BY = "superseded_by"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class KnowledgeLink:
|
|
56
|
+
"""A link between two civilization nodes."""
|
|
57
|
+
id: UUID = field(default_factory=uuid4)
|
|
58
|
+
source_id: UUID = field(default_factory=uuid4)
|
|
59
|
+
target_id: UUID = field(default_factory=uuid4)
|
|
60
|
+
link_type: LinkType = LinkType.RELATED_TO
|
|
61
|
+
strength: float = 1.0 # 0.0-1.0, indicates confidence/relevance
|
|
62
|
+
auto_detected: bool = False
|
|
63
|
+
description: str | None = None
|
|
64
|
+
metadata: dict = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
def inverse(self) -> "KnowledgeLink":
|
|
67
|
+
"""Get the inverse of this link (swap source and target, flip type)."""
|
|
68
|
+
inverse_types = {
|
|
69
|
+
LinkType.PARENT_OF: LinkType.CHILD_OF,
|
|
70
|
+
LinkType.CHILD_OF: LinkType.PARENT_OF,
|
|
71
|
+
LinkType.PRECEDES: LinkType.FOLLOWS,
|
|
72
|
+
LinkType.FOLLOWS: LinkType.PRECEDES,
|
|
73
|
+
LinkType.TRIGGERS: LinkType.TRIGGERED_BY,
|
|
74
|
+
LinkType.TRIGGERED_BY: LinkType.TRIGGERS,
|
|
75
|
+
LinkType.OWNS: LinkType.OWNED_BY,
|
|
76
|
+
LinkType.OWNED_BY: LinkType.OWNS,
|
|
77
|
+
LinkType.REFERENCES: LinkType.REFERENCED_BY,
|
|
78
|
+
LinkType.REFERENCED_BY: LinkType.REFERENCES,
|
|
79
|
+
LinkType.SUPERSEDES: LinkType.SUPERSEDED_BY,
|
|
80
|
+
LinkType.SUPERSEDED_BY: LinkType.SUPERSEDES,
|
|
81
|
+
}
|
|
82
|
+
return KnowledgeLink(
|
|
83
|
+
source_id=self.target_id,
|
|
84
|
+
target_id=self.source_id,
|
|
85
|
+
link_type=inverse_types.get(self.link_type, self.link_type),
|
|
86
|
+
strength=self.strength,
|
|
87
|
+
auto_detected=self.auto_detected,
|
|
88
|
+
metadata=self.metadata,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class KnowledgeLinker:
|
|
93
|
+
"""
|
|
94
|
+
Establishes and manages relationships between civilization nodes.
|
|
95
|
+
Supports automatic link detection and relationship inference.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, org_id: str, store=None):
|
|
99
|
+
self.org_id = org_id
|
|
100
|
+
self.store = store
|
|
101
|
+
self._links: list[KnowledgeLink] = []
|
|
102
|
+
self._link_index: dict[UUID, list[KnowledgeLink]] = {}
|
|
103
|
+
|
|
104
|
+
def add_link(self, link: KnowledgeLink) -> KnowledgeLink:
|
|
105
|
+
"""Add a link to the graph."""
|
|
106
|
+
self._links.append(link)
|
|
107
|
+
self._link_index.setdefault(link.source_id, []).append(link)
|
|
108
|
+
self._link_index.setdefault(link.target_id, []).append(link.inverse())
|
|
109
|
+
return link
|
|
110
|
+
|
|
111
|
+
def create_link(
|
|
112
|
+
self,
|
|
113
|
+
source_id: UUID,
|
|
114
|
+
target_id: UUID,
|
|
115
|
+
link_type: LinkType,
|
|
116
|
+
strength: float = 1.0,
|
|
117
|
+
description: str | None = None,
|
|
118
|
+
) -> KnowledgeLink:
|
|
119
|
+
"""Create and add a new link."""
|
|
120
|
+
link = KnowledgeLink(
|
|
121
|
+
source_id=source_id,
|
|
122
|
+
target_id=target_id,
|
|
123
|
+
link_type=link_type,
|
|
124
|
+
strength=strength,
|
|
125
|
+
description=description,
|
|
126
|
+
)
|
|
127
|
+
return self.add_link(link)
|
|
128
|
+
|
|
129
|
+
def get_links_from(self, node_id: UUID) -> list[KnowledgeLink]:
|
|
130
|
+
"""Get all links originating from a node."""
|
|
131
|
+
return [l for l in self._link_index.get(node_id, []) if l.source_id == node_id]
|
|
132
|
+
|
|
133
|
+
def get_links_to(self, node_id: UUID) -> list[KnowledgeLink]:
|
|
134
|
+
"""Get all links pointing to a node."""
|
|
135
|
+
return [l for l in self._links if l.target_id == node_id]
|
|
136
|
+
|
|
137
|
+
def get_all_links(self, node_id: UUID) -> list[KnowledgeLink]:
|
|
138
|
+
"""Get all links involving a node."""
|
|
139
|
+
return self._link_index.get(node_id, [])
|
|
140
|
+
|
|
141
|
+
def get_links_by_type(self, node_id: UUID, link_type: LinkType) -> list[KnowledgeLink]:
|
|
142
|
+
"""Get links of a specific type from a node."""
|
|
143
|
+
return [l for l in self.get_links_from(node_id) if l.link_type == link_type]
|
|
144
|
+
|
|
145
|
+
def detect_links(self, nodes: list[CivilizationNode]) -> list[KnowledgeLink]:
|
|
146
|
+
"""
|
|
147
|
+
Automatically detect links between nodes based on content analysis.
|
|
148
|
+
"""
|
|
149
|
+
detected_links: list[KnowledgeLink] = []
|
|
150
|
+
node_map = {n.id: n for n in nodes}
|
|
151
|
+
|
|
152
|
+
# Build lookup tables
|
|
153
|
+
role_map: dict[str, UUID] = {}
|
|
154
|
+
sop_map: dict[str, UUID] = {}
|
|
155
|
+
|
|
156
|
+
for node in nodes:
|
|
157
|
+
if isinstance(node, Role):
|
|
158
|
+
role_map[node.role_title.lower()] = node.id
|
|
159
|
+
elif isinstance(node, SOP):
|
|
160
|
+
sop_map[node.title.lower()] = node.id
|
|
161
|
+
|
|
162
|
+
# Detect SOP -> Role links
|
|
163
|
+
for node in nodes:
|
|
164
|
+
if isinstance(node, SOP):
|
|
165
|
+
for role_name in node.roles_involved:
|
|
166
|
+
role_id = role_map.get(role_name.lower())
|
|
167
|
+
if role_id:
|
|
168
|
+
link = KnowledgeLink(
|
|
169
|
+
source_id=node.id,
|
|
170
|
+
target_id=role_id,
|
|
171
|
+
link_type=LinkType.REQUIRES_ROLE,
|
|
172
|
+
auto_detected=True,
|
|
173
|
+
strength=0.9,
|
|
174
|
+
)
|
|
175
|
+
detected_links.append(link)
|
|
176
|
+
|
|
177
|
+
# Detect SOP -> SOP links (related_sops field)
|
|
178
|
+
for node in nodes:
|
|
179
|
+
if isinstance(node, SOP):
|
|
180
|
+
for related_id in node.related_sops:
|
|
181
|
+
if related_id in node_map:
|
|
182
|
+
link = KnowledgeLink(
|
|
183
|
+
source_id=node.id,
|
|
184
|
+
target_id=related_id,
|
|
185
|
+
link_type=LinkType.RELATED_TO,
|
|
186
|
+
auto_detected=True,
|
|
187
|
+
strength=0.8,
|
|
188
|
+
)
|
|
189
|
+
detected_links.append(link)
|
|
190
|
+
|
|
191
|
+
# Detect Checklist -> SOP links
|
|
192
|
+
for node in nodes:
|
|
193
|
+
if isinstance(node, Checklist) and node.related_sop_id:
|
|
194
|
+
if node.related_sop_id in node_map:
|
|
195
|
+
link = KnowledgeLink(
|
|
196
|
+
source_id=node.id,
|
|
197
|
+
target_id=node.related_sop_id,
|
|
198
|
+
link_type=LinkType.CHILD_OF,
|
|
199
|
+
auto_detected=True,
|
|
200
|
+
strength=1.0,
|
|
201
|
+
)
|
|
202
|
+
detected_links.append(link)
|
|
203
|
+
|
|
204
|
+
# Detect Person -> Role links
|
|
205
|
+
for node in nodes:
|
|
206
|
+
if isinstance(node, Person) and node.role_id:
|
|
207
|
+
if node.role_id in node_map:
|
|
208
|
+
link = KnowledgeLink(
|
|
209
|
+
source_id=node.id,
|
|
210
|
+
target_id=node.role_id,
|
|
211
|
+
link_type=LinkType.ASSIGNED_TO,
|
|
212
|
+
auto_detected=True,
|
|
213
|
+
strength=1.0,
|
|
214
|
+
)
|
|
215
|
+
detected_links.append(link)
|
|
216
|
+
|
|
217
|
+
# Detect text-based links using title/content matching
|
|
218
|
+
detected_links.extend(self._detect_text_links(nodes))
|
|
219
|
+
|
|
220
|
+
return detected_links
|
|
221
|
+
|
|
222
|
+
def _detect_text_links(self, nodes: list[CivilizationNode]) -> list[KnowledgeLink]:
|
|
223
|
+
"""Detect links based on text content analysis."""
|
|
224
|
+
links = []
|
|
225
|
+
|
|
226
|
+
# Build title index
|
|
227
|
+
title_to_id: dict[str, UUID] = {}
|
|
228
|
+
for node in nodes:
|
|
229
|
+
# Normalize title
|
|
230
|
+
title_key = node.title.lower().strip()
|
|
231
|
+
title_to_id[title_key] = node.id
|
|
232
|
+
|
|
233
|
+
# Look for mentions of other nodes' titles in content
|
|
234
|
+
for node in nodes:
|
|
235
|
+
content = self._get_node_content(node)
|
|
236
|
+
if not content:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
for title, target_id in title_to_id.items():
|
|
240
|
+
if target_id == node.id:
|
|
241
|
+
continue
|
|
242
|
+
# Simple substring matching (could be enhanced with NLP)
|
|
243
|
+
if title in content.lower() and len(title) > 5:
|
|
244
|
+
links.append(KnowledgeLink(
|
|
245
|
+
source_id=node.id,
|
|
246
|
+
target_id=target_id,
|
|
247
|
+
link_type=LinkType.REFERENCES,
|
|
248
|
+
auto_detected=True,
|
|
249
|
+
strength=0.6,
|
|
250
|
+
))
|
|
251
|
+
|
|
252
|
+
return links
|
|
253
|
+
|
|
254
|
+
def _get_node_content(self, node: CivilizationNode) -> str:
|
|
255
|
+
"""Extract searchable text content from a node."""
|
|
256
|
+
parts = [node.title]
|
|
257
|
+
|
|
258
|
+
if isinstance(node, SOP):
|
|
259
|
+
parts.append(node.purpose)
|
|
260
|
+
parts.append(node.scope)
|
|
261
|
+
for step in node.steps:
|
|
262
|
+
parts.append(step.title)
|
|
263
|
+
parts.append(step.description)
|
|
264
|
+
elif isinstance(node, Role):
|
|
265
|
+
parts.extend(node.responsibilities)
|
|
266
|
+
parts.extend(node.accountabilities)
|
|
267
|
+
elif isinstance(node, JobDescription):
|
|
268
|
+
parts.append(node.summary)
|
|
269
|
+
parts.extend(node.responsibilities)
|
|
270
|
+
|
|
271
|
+
return " ".join(filter(None, parts))
|
|
272
|
+
|
|
273
|
+
def find_related(
|
|
274
|
+
self,
|
|
275
|
+
node_id: UUID,
|
|
276
|
+
max_depth: int = 2,
|
|
277
|
+
link_types: list[LinkType] | None = None,
|
|
278
|
+
) -> list[tuple[UUID, int, LinkType]]:
|
|
279
|
+
"""
|
|
280
|
+
Find all related nodes within a certain graph distance.
|
|
281
|
+
Returns list of (node_id, distance, link_type).
|
|
282
|
+
"""
|
|
283
|
+
results: list[tuple[UUID, int, LinkType]] = []
|
|
284
|
+
visited = {node_id}
|
|
285
|
+
queue: list[tuple[UUID, int]] = [(node_id, 0)]
|
|
286
|
+
|
|
287
|
+
while queue:
|
|
288
|
+
current_id, depth = queue.pop(0)
|
|
289
|
+
if depth >= max_depth:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
for link in self.get_links_from(current_id):
|
|
293
|
+
if link_types and link.link_type not in link_types:
|
|
294
|
+
continue
|
|
295
|
+
if link.target_id not in visited:
|
|
296
|
+
visited.add(link.target_id)
|
|
297
|
+
results.append((link.target_id, depth + 1, link.link_type))
|
|
298
|
+
queue.append((link.target_id, depth + 1))
|
|
299
|
+
|
|
300
|
+
return results
|
|
301
|
+
|
|
302
|
+
def compute_importance(self, node_id: UUID) -> float:
|
|
303
|
+
"""
|
|
304
|
+
Compute a simple importance score based on incoming links.
|
|
305
|
+
Similar to PageRank but simplified.
|
|
306
|
+
"""
|
|
307
|
+
incoming = self.get_links_to(node_id)
|
|
308
|
+
# Weight by link strength
|
|
309
|
+
return sum(l.strength for l in incoming)
|
|
310
|
+
|
|
311
|
+
def get_link_statistics(self) -> dict:
|
|
312
|
+
"""Get statistics about links in the graph."""
|
|
313
|
+
type_counts: dict[str, int] = {}
|
|
314
|
+
for link in self._links:
|
|
315
|
+
type_counts[link.link_type.value] = type_counts.get(link.link_type.value, 0) + 1
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
"total_links": len(self._links),
|
|
319
|
+
"auto_detected": sum(1 for l in self._links if l.auto_detected),
|
|
320
|
+
"manual": sum(1 for l in self._links if not l.auto_detected),
|
|
321
|
+
"by_type": type_counts,
|
|
322
|
+
"average_strength": sum(l.strength for l in self._links) / len(self._links) if self._links else 0,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
def to_dict(self) -> dict:
|
|
326
|
+
"""Export all links to dictionary format."""
|
|
327
|
+
return {
|
|
328
|
+
"org_id": self.org_id,
|
|
329
|
+
"links": [
|
|
330
|
+
{
|
|
331
|
+
"id": str(l.id),
|
|
332
|
+
"source_id": str(l.source_id),
|
|
333
|
+
"target_id": str(l.target_id),
|
|
334
|
+
"link_type": l.link_type.value,
|
|
335
|
+
"strength": l.strength,
|
|
336
|
+
"auto_detected": l.auto_detected,
|
|
337
|
+
"description": l.description,
|
|
338
|
+
"metadata": l.metadata,
|
|
339
|
+
}
|
|
340
|
+
for l in self._links
|
|
341
|
+
],
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
def bulk_add_links(self, links: list[KnowledgeLink]) -> int:
|
|
345
|
+
"""Add multiple links at once. Returns count of links added."""
|
|
346
|
+
for link in links:
|
|
347
|
+
self.add_link(link)
|
|
348
|
+
return len(links)
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organizational graph for representing reporting structures, teams, and hierarchies.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Iterator
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
from ..schema import Person, Role, OrgChart, OrgUnit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrgNodeType(str, Enum):
|
|
17
|
+
PERSON = "person"
|
|
18
|
+
ROLE = "role"
|
|
19
|
+
TEAM = "team"
|
|
20
|
+
DEPARTMENT = "department"
|
|
21
|
+
DIVISION = "division"
|
|
22
|
+
ORGANIZATION = "organization"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OrgEdgeType(str, Enum):
|
|
26
|
+
REPORTS_TO = "reports_to"
|
|
27
|
+
MANAGES = "manages"
|
|
28
|
+
MEMBER_OF = "member_of"
|
|
29
|
+
LEADS = "leads"
|
|
30
|
+
COLLABORATES_WITH = "collaborates_with"
|
|
31
|
+
DOTTED_LINE_TO = "dotted_line_to"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class OrgNode:
|
|
36
|
+
"""A node in the organizational graph."""
|
|
37
|
+
id: UUID
|
|
38
|
+
node_type: OrgNodeType
|
|
39
|
+
name: str
|
|
40
|
+
title: str | None = None
|
|
41
|
+
department: str | None = None
|
|
42
|
+
level: int = 0 # Hierarchy level (0 = root)
|
|
43
|
+
metadata: dict = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def __hash__(self):
|
|
46
|
+
return hash(self.id)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class OrgEdge:
|
|
51
|
+
"""An edge connecting two organizational nodes."""
|
|
52
|
+
source_id: UUID
|
|
53
|
+
target_id: UUID
|
|
54
|
+
edge_type: OrgEdgeType
|
|
55
|
+
weight: float = 1.0
|
|
56
|
+
metadata: dict = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def __hash__(self):
|
|
59
|
+
return hash((self.source_id, self.target_id, self.edge_type))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OrgGraph:
|
|
63
|
+
"""
|
|
64
|
+
Graph representation of organizational structure.
|
|
65
|
+
Supports hierarchy traversal, team composition queries, and path finding.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, org_id: str):
|
|
69
|
+
self.org_id = org_id
|
|
70
|
+
self._nodes: dict[UUID, OrgNode] = {}
|
|
71
|
+
self._edges: list[OrgEdge] = []
|
|
72
|
+
self._adjacency: dict[UUID, list[OrgEdge]] = {}
|
|
73
|
+
self._reverse_adjacency: dict[UUID, list[OrgEdge]] = {}
|
|
74
|
+
|
|
75
|
+
def add_node(self, node: OrgNode) -> None:
|
|
76
|
+
"""Add a node to the graph."""
|
|
77
|
+
self._nodes[node.id] = node
|
|
78
|
+
if node.id not in self._adjacency:
|
|
79
|
+
self._adjacency[node.id] = []
|
|
80
|
+
if node.id not in self._reverse_adjacency:
|
|
81
|
+
self._reverse_adjacency[node.id] = []
|
|
82
|
+
|
|
83
|
+
def add_edge(self, edge: OrgEdge) -> None:
|
|
84
|
+
"""Add an edge to the graph."""
|
|
85
|
+
if edge.source_id not in self._nodes or edge.target_id not in self._nodes:
|
|
86
|
+
raise ValueError("Both source and target nodes must exist in the graph")
|
|
87
|
+
self._edges.append(edge)
|
|
88
|
+
self._adjacency[edge.source_id].append(edge)
|
|
89
|
+
self._reverse_adjacency[edge.target_id].append(edge)
|
|
90
|
+
|
|
91
|
+
def get_node(self, node_id: UUID) -> OrgNode | None:
|
|
92
|
+
"""Get a node by ID."""
|
|
93
|
+
return self._nodes.get(node_id)
|
|
94
|
+
|
|
95
|
+
def get_edges_from(self, node_id: UUID) -> list[OrgEdge]:
|
|
96
|
+
"""Get all edges originating from a node."""
|
|
97
|
+
return self._adjacency.get(node_id, [])
|
|
98
|
+
|
|
99
|
+
def get_edges_to(self, node_id: UUID) -> list[OrgEdge]:
|
|
100
|
+
"""Get all edges pointing to a node."""
|
|
101
|
+
return self._reverse_adjacency.get(node_id, [])
|
|
102
|
+
|
|
103
|
+
def get_manager(self, person_id: UUID) -> OrgNode | None:
|
|
104
|
+
"""Get the manager of a person."""
|
|
105
|
+
edges = self.get_edges_from(person_id)
|
|
106
|
+
for edge in edges:
|
|
107
|
+
if edge.edge_type == OrgEdgeType.REPORTS_TO:
|
|
108
|
+
return self.get_node(edge.target_id)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def get_direct_reports(self, manager_id: UUID) -> list[OrgNode]:
|
|
112
|
+
"""Get all direct reports of a manager."""
|
|
113
|
+
edges = self.get_edges_to(manager_id)
|
|
114
|
+
reports = []
|
|
115
|
+
for edge in edges:
|
|
116
|
+
if edge.edge_type == OrgEdgeType.REPORTS_TO:
|
|
117
|
+
node = self.get_node(edge.source_id)
|
|
118
|
+
if node:
|
|
119
|
+
reports.append(node)
|
|
120
|
+
return reports
|
|
121
|
+
|
|
122
|
+
def get_team_members(self, team_id: UUID) -> list[OrgNode]:
|
|
123
|
+
"""Get all members of a team."""
|
|
124
|
+
edges = self.get_edges_to(team_id)
|
|
125
|
+
members = []
|
|
126
|
+
for edge in edges:
|
|
127
|
+
if edge.edge_type == OrgEdgeType.MEMBER_OF:
|
|
128
|
+
node = self.get_node(edge.source_id)
|
|
129
|
+
if node:
|
|
130
|
+
members.append(node)
|
|
131
|
+
return members
|
|
132
|
+
|
|
133
|
+
def get_reporting_chain(self, person_id: UUID) -> list[OrgNode]:
|
|
134
|
+
"""Get the full reporting chain from person to CEO."""
|
|
135
|
+
chain = []
|
|
136
|
+
current = self.get_node(person_id)
|
|
137
|
+
visited = set()
|
|
138
|
+
|
|
139
|
+
while current and current.id not in visited:
|
|
140
|
+
visited.add(current.id)
|
|
141
|
+
manager = self.get_manager(current.id)
|
|
142
|
+
if manager:
|
|
143
|
+
chain.append(manager)
|
|
144
|
+
current = manager
|
|
145
|
+
else:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
return chain
|
|
149
|
+
|
|
150
|
+
def get_all_descendants(self, node_id: UUID) -> list[OrgNode]:
|
|
151
|
+
"""Get all descendants (direct and indirect reports) of a node."""
|
|
152
|
+
descendants = []
|
|
153
|
+
to_visit = [node_id]
|
|
154
|
+
visited = set()
|
|
155
|
+
|
|
156
|
+
while to_visit:
|
|
157
|
+
current_id = to_visit.pop(0)
|
|
158
|
+
if current_id in visited:
|
|
159
|
+
continue
|
|
160
|
+
visited.add(current_id)
|
|
161
|
+
|
|
162
|
+
direct_reports = self.get_direct_reports(current_id)
|
|
163
|
+
for report in direct_reports:
|
|
164
|
+
descendants.append(report)
|
|
165
|
+
to_visit.append(report.id)
|
|
166
|
+
|
|
167
|
+
return descendants
|
|
168
|
+
|
|
169
|
+
def find_common_manager(self, person1_id: UUID, person2_id: UUID) -> OrgNode | None:
|
|
170
|
+
"""Find the lowest common manager of two people."""
|
|
171
|
+
chain1 = set([person1_id] + [n.id for n in self.get_reporting_chain(person1_id)])
|
|
172
|
+
chain2 = self.get_reporting_chain(person2_id)
|
|
173
|
+
|
|
174
|
+
for manager in chain2:
|
|
175
|
+
if manager.id in chain1:
|
|
176
|
+
return manager
|
|
177
|
+
|
|
178
|
+
# Check if person1 is in person2's chain
|
|
179
|
+
if person1_id in set(n.id for n in chain2):
|
|
180
|
+
return self.get_node(person1_id)
|
|
181
|
+
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def calculate_distance(self, node1_id: UUID, node2_id: UUID) -> int | None:
|
|
185
|
+
"""Calculate organizational distance between two nodes (BFS)."""
|
|
186
|
+
if node1_id == node2_id:
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
visited = {node1_id}
|
|
190
|
+
queue = [(node1_id, 0)]
|
|
191
|
+
|
|
192
|
+
while queue:
|
|
193
|
+
current_id, distance = queue.pop(0)
|
|
194
|
+
|
|
195
|
+
# Check all adjacent nodes (both directions)
|
|
196
|
+
neighbors = []
|
|
197
|
+
for edge in self.get_edges_from(current_id):
|
|
198
|
+
neighbors.append(edge.target_id)
|
|
199
|
+
for edge in self.get_edges_to(current_id):
|
|
200
|
+
neighbors.append(edge.source_id)
|
|
201
|
+
|
|
202
|
+
for neighbor_id in neighbors:
|
|
203
|
+
if neighbor_id == node2_id:
|
|
204
|
+
return distance + 1
|
|
205
|
+
if neighbor_id not in visited:
|
|
206
|
+
visited.add(neighbor_id)
|
|
207
|
+
queue.append((neighbor_id, distance + 1))
|
|
208
|
+
|
|
209
|
+
return None # No path found
|
|
210
|
+
|
|
211
|
+
def get_department_members(self, department: str) -> list[OrgNode]:
|
|
212
|
+
"""Get all nodes in a department."""
|
|
213
|
+
return [n for n in self._nodes.values() if n.department == department]
|
|
214
|
+
|
|
215
|
+
def get_nodes_by_type(self, node_type: OrgNodeType) -> list[OrgNode]:
|
|
216
|
+
"""Get all nodes of a specific type."""
|
|
217
|
+
return [n for n in self._nodes.values() if n.node_type == node_type]
|
|
218
|
+
|
|
219
|
+
def calculate_span_of_control(self, manager_id: UUID) -> int:
|
|
220
|
+
"""Calculate span of control (number of direct reports)."""
|
|
221
|
+
return len(self.get_direct_reports(manager_id))
|
|
222
|
+
|
|
223
|
+
def calculate_depth(self) -> int:
|
|
224
|
+
"""Calculate maximum depth of the org hierarchy."""
|
|
225
|
+
max_depth = 0
|
|
226
|
+
for node in self._nodes.values():
|
|
227
|
+
chain = self.get_reporting_chain(node.id)
|
|
228
|
+
max_depth = max(max_depth, len(chain) + 1)
|
|
229
|
+
return max_depth
|
|
230
|
+
|
|
231
|
+
def to_dict(self) -> dict:
|
|
232
|
+
"""Export graph to dictionary format."""
|
|
233
|
+
return {
|
|
234
|
+
"org_id": self.org_id,
|
|
235
|
+
"nodes": [
|
|
236
|
+
{
|
|
237
|
+
"id": str(n.id),
|
|
238
|
+
"node_type": n.node_type.value,
|
|
239
|
+
"name": n.name,
|
|
240
|
+
"title": n.title,
|
|
241
|
+
"department": n.department,
|
|
242
|
+
"level": n.level,
|
|
243
|
+
"metadata": n.metadata,
|
|
244
|
+
}
|
|
245
|
+
for n in self._nodes.values()
|
|
246
|
+
],
|
|
247
|
+
"edges": [
|
|
248
|
+
{
|
|
249
|
+
"source_id": str(e.source_id),
|
|
250
|
+
"target_id": str(e.target_id),
|
|
251
|
+
"edge_type": e.edge_type.value,
|
|
252
|
+
"weight": e.weight,
|
|
253
|
+
"metadata": e.metadata,
|
|
254
|
+
}
|
|
255
|
+
for e in self._edges
|
|
256
|
+
],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def from_org_chart(cls, org_chart: OrgChart) -> "OrgGraph":
|
|
261
|
+
"""Build an OrgGraph from an OrgChart node."""
|
|
262
|
+
graph = cls(org_id=org_chart.org_id)
|
|
263
|
+
|
|
264
|
+
# Add org root
|
|
265
|
+
org_node = OrgNode(
|
|
266
|
+
id=org_chart.id,
|
|
267
|
+
node_type=OrgNodeType.ORGANIZATION,
|
|
268
|
+
name=org_chart.organization_name,
|
|
269
|
+
level=0,
|
|
270
|
+
)
|
|
271
|
+
graph.add_node(org_node)
|
|
272
|
+
|
|
273
|
+
# Process units
|
|
274
|
+
unit_nodes: dict[str, OrgNode] = {}
|
|
275
|
+
for unit in org_chart.units:
|
|
276
|
+
unit_type_map = {
|
|
277
|
+
"division": OrgNodeType.DIVISION,
|
|
278
|
+
"department": OrgNodeType.DEPARTMENT,
|
|
279
|
+
"team": OrgNodeType.TEAM,
|
|
280
|
+
}
|
|
281
|
+
unit_node = OrgNode(
|
|
282
|
+
id=uuid4(),
|
|
283
|
+
node_type=unit_type_map.get(unit.unit_type, OrgNodeType.TEAM),
|
|
284
|
+
name=unit.name,
|
|
285
|
+
metadata={"unit_id": unit.unit_id, "mission": unit.mission},
|
|
286
|
+
)
|
|
287
|
+
graph.add_node(unit_node)
|
|
288
|
+
unit_nodes[unit.unit_id] = unit_node
|
|
289
|
+
|
|
290
|
+
# Add hierarchy edges
|
|
291
|
+
for unit in org_chart.units:
|
|
292
|
+
unit_node = unit_nodes[unit.unit_id]
|
|
293
|
+
if unit.parent_unit_id:
|
|
294
|
+
parent = unit_nodes.get(unit.parent_unit_id)
|
|
295
|
+
if parent:
|
|
296
|
+
graph.add_edge(OrgEdge(
|
|
297
|
+
source_id=unit_node.id,
|
|
298
|
+
target_id=parent.id,
|
|
299
|
+
edge_type=OrgEdgeType.MEMBER_OF,
|
|
300
|
+
))
|
|
301
|
+
else:
|
|
302
|
+
# Top-level unit reports to org
|
|
303
|
+
graph.add_edge(OrgEdge(
|
|
304
|
+
source_id=unit_node.id,
|
|
305
|
+
target_id=org_chart.id,
|
|
306
|
+
edge_type=OrgEdgeType.MEMBER_OF,
|
|
307
|
+
))
|
|
308
|
+
|
|
309
|
+
return graph
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def from_people(cls, org_id: str, people: list[Person]) -> "OrgGraph":
|
|
313
|
+
"""Build an OrgGraph from a list of Person nodes."""
|
|
314
|
+
graph = cls(org_id=org_id)
|
|
315
|
+
|
|
316
|
+
# Add all people as nodes
|
|
317
|
+
for person in people:
|
|
318
|
+
node = OrgNode(
|
|
319
|
+
id=person.id,
|
|
320
|
+
node_type=OrgNodeType.PERSON,
|
|
321
|
+
name=person.name,
|
|
322
|
+
title=person.department,
|
|
323
|
+
department=person.department,
|
|
324
|
+
metadata={
|
|
325
|
+
"email": person.email,
|
|
326
|
+
"location": person.location,
|
|
327
|
+
"timezone": person.timezone,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
graph.add_node(node)
|
|
331
|
+
|
|
332
|
+
# Add reporting relationships
|
|
333
|
+
person_map = {p.id: p for p in people}
|
|
334
|
+
for person in people:
|
|
335
|
+
if person.manager_id and person.manager_id in person_map:
|
|
336
|
+
graph.add_edge(OrgEdge(
|
|
337
|
+
source_id=person.id,
|
|
338
|
+
target_id=person.manager_id,
|
|
339
|
+
edge_type=OrgEdgeType.REPORTS_TO,
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
return graph
|
|
343
|
+
|
|
344
|
+
def __len__(self) -> int:
|
|
345
|
+
return len(self._nodes)
|
|
346
|
+
|
|
347
|
+
def __iter__(self) -> Iterator[OrgNode]:
|
|
348
|
+
return iter(self._nodes.values())
|