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.
Files changed (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. 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())