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,386 @@
1
+ """
2
+ Process graph for representing workflows, SOPs, and procedural knowledge.
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 SOP, SOPStep, Workflow, WorkflowNode, WorkflowEdge, Checklist
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ProcessNodeType(str, Enum):
17
+ START = "start"
18
+ END = "end"
19
+ TASK = "task"
20
+ DECISION = "decision"
21
+ APPROVAL = "approval"
22
+ SUBPROCESS = "subprocess"
23
+ PARALLEL_GATEWAY = "parallel_gateway"
24
+ JOIN_GATEWAY = "join_gateway"
25
+ EVENT = "event"
26
+ WAIT = "wait"
27
+
28
+
29
+ class ProcessEdgeType(str, Enum):
30
+ SEQUENCE = "sequence"
31
+ CONDITIONAL = "conditional"
32
+ DEFAULT = "default"
33
+ EXCEPTION = "exception"
34
+ TIMEOUT = "timeout"
35
+
36
+
37
+ @dataclass
38
+ class ProcessNode:
39
+ """A node in the process graph."""
40
+ id: str
41
+ node_type: ProcessNodeType
42
+ label: str
43
+ description: str | None = None
44
+ responsible_role: str | None = None
45
+ tools: list[str] = field(default_factory=list)
46
+ duration_minutes: int | None = None
47
+ sla_minutes: int | None = None
48
+ metadata: dict = field(default_factory=dict)
49
+
50
+ def __hash__(self):
51
+ return hash(self.id)
52
+
53
+
54
+ @dataclass
55
+ class ProcessEdge:
56
+ """An edge connecting two process nodes."""
57
+ source_id: str
58
+ target_id: str
59
+ edge_type: ProcessEdgeType = ProcessEdgeType.SEQUENCE
60
+ condition: str | None = None
61
+ probability: float | None = None # For decision edges
62
+ metadata: dict = field(default_factory=dict)
63
+
64
+ def __hash__(self):
65
+ return hash((self.source_id, self.target_id))
66
+
67
+
68
+ class ProcessGraph:
69
+ """
70
+ Graph representation of a process or workflow.
71
+ Supports path analysis, critical path detection, and process validation.
72
+ """
73
+
74
+ def __init__(self, process_id: UUID, name: str):
75
+ self.process_id = process_id
76
+ self.name = name
77
+ self._nodes: dict[str, ProcessNode] = {}
78
+ self._edges: list[ProcessEdge] = []
79
+ self._adjacency: dict[str, list[ProcessEdge]] = {}
80
+ self._reverse_adjacency: dict[str, list[ProcessEdge]] = {}
81
+
82
+ def add_node(self, node: ProcessNode) -> None:
83
+ """Add a node to the graph."""
84
+ self._nodes[node.id] = node
85
+ if node.id not in self._adjacency:
86
+ self._adjacency[node.id] = []
87
+ if node.id not in self._reverse_adjacency:
88
+ self._reverse_adjacency[node.id] = []
89
+
90
+ def add_edge(self, edge: ProcessEdge) -> None:
91
+ """Add an edge to the graph."""
92
+ if edge.source_id not in self._nodes or edge.target_id not in self._nodes:
93
+ raise ValueError("Both source and target nodes must exist in the graph")
94
+ self._edges.append(edge)
95
+ self._adjacency[edge.source_id].append(edge)
96
+ self._reverse_adjacency[edge.target_id].append(edge)
97
+
98
+ def get_node(self, node_id: str) -> ProcessNode | None:
99
+ """Get a node by ID."""
100
+ return self._nodes.get(node_id)
101
+
102
+ def get_start_nodes(self) -> list[ProcessNode]:
103
+ """Get all start nodes."""
104
+ return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.START]
105
+
106
+ def get_end_nodes(self) -> list[ProcessNode]:
107
+ """Get all end nodes."""
108
+ return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.END]
109
+
110
+ def get_successors(self, node_id: str) -> list[ProcessNode]:
111
+ """Get all successor nodes."""
112
+ edges = self._adjacency.get(node_id, [])
113
+ return [self._nodes[e.target_id] for e in edges if e.target_id in self._nodes]
114
+
115
+ def get_predecessors(self, node_id: str) -> list[ProcessNode]:
116
+ """Get all predecessor nodes."""
117
+ edges = self._reverse_adjacency.get(node_id, [])
118
+ return [self._nodes[e.source_id] for e in edges if e.source_id in self._nodes]
119
+
120
+ def get_decision_nodes(self) -> list[ProcessNode]:
121
+ """Get all decision nodes."""
122
+ return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.DECISION]
123
+
124
+ def find_all_paths(
125
+ self,
126
+ start_id: str | None = None,
127
+ end_id: str | None = None,
128
+ ) -> list[list[str]]:
129
+ """Find all paths from start to end nodes."""
130
+ starts = [start_id] if start_id else [n.id for n in self.get_start_nodes()]
131
+ ends = set([end_id]) if end_id else set(n.id for n in self.get_end_nodes())
132
+
133
+ all_paths = []
134
+
135
+ for start in starts:
136
+ stack = [(start, [start])]
137
+ while stack:
138
+ current, path = stack.pop()
139
+ if current in ends:
140
+ all_paths.append(path)
141
+ continue
142
+ for edge in self._adjacency.get(current, []):
143
+ if edge.target_id not in path: # Avoid cycles
144
+ stack.append((edge.target_id, path + [edge.target_id]))
145
+
146
+ return all_paths
147
+
148
+ def calculate_critical_path(self) -> list[ProcessNode]:
149
+ """
150
+ Calculate the critical path (longest path through the process).
151
+ Uses node durations if available.
152
+ """
153
+ all_paths = self.find_all_paths()
154
+ if not all_paths:
155
+ return []
156
+
157
+ def path_duration(path: list[str]) -> int:
158
+ total = 0
159
+ for node_id in path:
160
+ node = self._nodes.get(node_id)
161
+ if node and node.duration_minutes:
162
+ total += node.duration_minutes
163
+ return total
164
+
165
+ critical = max(all_paths, key=path_duration)
166
+ return [self._nodes[node_id] for node_id in critical]
167
+
168
+ def estimate_total_duration(self) -> int:
169
+ """Estimate total process duration based on critical path."""
170
+ critical = self.calculate_critical_path()
171
+ return sum(n.duration_minutes or 0 for n in critical)
172
+
173
+ def validate(self) -> list[str]:
174
+ """
175
+ Validate the process graph structure.
176
+ Returns list of validation issues.
177
+ """
178
+ issues = []
179
+
180
+ # Check for start node
181
+ starts = self.get_start_nodes()
182
+ if not starts:
183
+ issues.append("Process has no START node")
184
+ elif len(starts) > 1:
185
+ issues.append(f"Process has {len(starts)} START nodes (expected 1)")
186
+
187
+ # Check for end node
188
+ ends = self.get_end_nodes()
189
+ if not ends:
190
+ issues.append("Process has no END node")
191
+
192
+ # Check for unreachable nodes
193
+ if starts:
194
+ reachable = self._get_reachable_nodes(starts[0].id)
195
+ unreachable = set(self._nodes.keys()) - reachable
196
+ if unreachable:
197
+ issues.append(f"Unreachable nodes: {unreachable}")
198
+
199
+ # Check for dead ends (non-end nodes with no outgoing edges)
200
+ for node_id, node in self._nodes.items():
201
+ if node.node_type != ProcessNodeType.END:
202
+ if not self._adjacency.get(node_id):
203
+ issues.append(f"Dead end: {node_id} ({node.label})")
204
+
205
+ # Check for cycles
206
+ if self._has_cycle():
207
+ issues.append("Process contains a cycle")
208
+
209
+ return issues
210
+
211
+ def _get_reachable_nodes(self, start_id: str) -> set[str]:
212
+ """Get all nodes reachable from a starting node."""
213
+ visited = set()
214
+ stack = [start_id]
215
+
216
+ while stack:
217
+ current = stack.pop()
218
+ if current in visited:
219
+ continue
220
+ visited.add(current)
221
+ for edge in self._adjacency.get(current, []):
222
+ stack.append(edge.target_id)
223
+
224
+ return visited
225
+
226
+ def _has_cycle(self) -> bool:
227
+ """Check if graph has any cycles using DFS."""
228
+ visited = set()
229
+ rec_stack = set()
230
+
231
+ def dfs(node_id: str) -> bool:
232
+ visited.add(node_id)
233
+ rec_stack.add(node_id)
234
+
235
+ for edge in self._adjacency.get(node_id, []):
236
+ if edge.target_id not in visited:
237
+ if dfs(edge.target_id):
238
+ return True
239
+ elif edge.target_id in rec_stack:
240
+ return True
241
+
242
+ rec_stack.remove(node_id)
243
+ return False
244
+
245
+ for node_id in self._nodes:
246
+ if node_id not in visited:
247
+ if dfs(node_id):
248
+ return True
249
+
250
+ return False
251
+
252
+ def get_roles_involved(self) -> list[str]:
253
+ """Get all roles involved in the process."""
254
+ roles = set()
255
+ for node in self._nodes.values():
256
+ if node.responsible_role:
257
+ roles.add(node.responsible_role)
258
+ return list(roles)
259
+
260
+ def get_tools_used(self) -> list[str]:
261
+ """Get all tools used in the process."""
262
+ tools = set()
263
+ for node in self._nodes.values():
264
+ tools.update(node.tools)
265
+ return list(tools)
266
+
267
+ def get_node_by_role(self, role: str) -> list[ProcessNode]:
268
+ """Get all nodes assigned to a specific role."""
269
+ return [n for n in self._nodes.values() if n.responsible_role == role]
270
+
271
+ def to_dict(self) -> dict:
272
+ """Export graph to dictionary format."""
273
+ return {
274
+ "process_id": str(self.process_id),
275
+ "name": self.name,
276
+ "nodes": [
277
+ {
278
+ "id": n.id,
279
+ "node_type": n.node_type.value,
280
+ "label": n.label,
281
+ "description": n.description,
282
+ "responsible_role": n.responsible_role,
283
+ "tools": n.tools,
284
+ "duration_minutes": n.duration_minutes,
285
+ "metadata": n.metadata,
286
+ }
287
+ for n in self._nodes.values()
288
+ ],
289
+ "edges": [
290
+ {
291
+ "source_id": e.source_id,
292
+ "target_id": e.target_id,
293
+ "edge_type": e.edge_type.value,
294
+ "condition": e.condition,
295
+ "metadata": e.metadata,
296
+ }
297
+ for e in self._edges
298
+ ],
299
+ }
300
+
301
+ @classmethod
302
+ def from_sop(cls, sop: SOP) -> "ProcessGraph":
303
+ """Build a ProcessGraph from an SOP."""
304
+ graph = cls(process_id=sop.id, name=sop.title)
305
+
306
+ # Add start node
307
+ start = ProcessNode(
308
+ id="start",
309
+ node_type=ProcessNodeType.START,
310
+ label="Start",
311
+ description=sop.purpose,
312
+ )
313
+ graph.add_node(start)
314
+
315
+ # Add step nodes
316
+ prev_id = "start"
317
+ for step in sop.steps:
318
+ step_id = f"step_{step.step_number}"
319
+ node = ProcessNode(
320
+ id=step_id,
321
+ node_type=ProcessNodeType.TASK,
322
+ label=step.title,
323
+ description=step.description,
324
+ responsible_role=step.responsible_role,
325
+ tools=step.tools_required,
326
+ duration_minutes=step.estimated_duration_minutes,
327
+ )
328
+ graph.add_node(node)
329
+ graph.add_edge(ProcessEdge(source_id=prev_id, target_id=step_id))
330
+ prev_id = step_id
331
+
332
+ # Add end node
333
+ end = ProcessNode(
334
+ id="end",
335
+ node_type=ProcessNodeType.END,
336
+ label="End",
337
+ )
338
+ graph.add_node(end)
339
+ graph.add_edge(ProcessEdge(source_id=prev_id, target_id="end"))
340
+
341
+ return graph
342
+
343
+ @classmethod
344
+ def from_workflow(cls, workflow: Workflow) -> "ProcessGraph":
345
+ """Build a ProcessGraph from a Workflow node."""
346
+ graph = cls(process_id=workflow.id, name=workflow.title)
347
+
348
+ # Map workflow node types to process node types
349
+ type_map = {
350
+ "start": ProcessNodeType.START,
351
+ "end": ProcessNodeType.END,
352
+ "task": ProcessNodeType.TASK,
353
+ "decision": ProcessNodeType.DECISION,
354
+ "approval": ProcessNodeType.APPROVAL,
355
+ }
356
+
357
+ # Add nodes
358
+ for wf_node in workflow.nodes:
359
+ node = ProcessNode(
360
+ id=wf_node.node_id,
361
+ node_type=type_map.get(wf_node.node_type, ProcessNodeType.TASK),
362
+ label=wf_node.label,
363
+ description=wf_node.description,
364
+ responsible_role=wf_node.responsible_role,
365
+ tools=[wf_node.tool] if wf_node.tool else [],
366
+ )
367
+ graph.add_node(node)
368
+
369
+ # Add edges
370
+ for wf_edge in workflow.edges:
371
+ edge_type = ProcessEdgeType.CONDITIONAL if wf_edge.condition else ProcessEdgeType.SEQUENCE
372
+ edge = ProcessEdge(
373
+ source_id=wf_edge.from_node,
374
+ target_id=wf_edge.to_node,
375
+ edge_type=edge_type,
376
+ condition=wf_edge.condition,
377
+ )
378
+ graph.add_edge(edge)
379
+
380
+ return graph
381
+
382
+ def __len__(self) -> int:
383
+ return len(self._nodes)
384
+
385
+ def __iter__(self) -> Iterator[ProcessNode]:
386
+ return iter(self._nodes.values())
@@ -0,0 +1,20 @@
1
+ """
2
+ Ingestion module for Civilization Knowledge Layer.
3
+ Handles document parsing, chunking, extraction, and embedding.
4
+ """
5
+ from .chunker import ProceduralChunker, SOPChunk, ChecklistChunk, RunbookChunk, GenericChunk
6
+ from .extractor import MetadataExtractor, EntityExtractor
7
+ from .embedder import CivilizationEmbedder
8
+ from .document_ingestor import DocumentIngestor
9
+
10
+ __all__ = [
11
+ "ProceduralChunker",
12
+ "SOPChunk",
13
+ "ChecklistChunk",
14
+ "RunbookChunk",
15
+ "GenericChunk",
16
+ "MetadataExtractor",
17
+ "EntityExtractor",
18
+ "CivilizationEmbedder",
19
+ "DocumentIngestor",
20
+ ]
@@ -0,0 +1,273 @@
1
+ """
2
+ Procedural-aware chunker. Preserves step order and injects surrounding
3
+ context into each chunk so agents can retrieve step N without losing
4
+ the fact that step N-1 must happen first.
5
+ """
6
+ from __future__ import annotations
7
+ from dataclasses import dataclass, field
8
+ from uuid import UUID
9
+ from typing import Any
10
+
11
+ from ..schema import SOP, SOPStep, Checklist, ChecklistItem, Runbook, RunbookStep, CivilizationNode
12
+
13
+
14
+ @dataclass
15
+ class SOPChunk:
16
+ sop_id: UUID
17
+ sop_title: str
18
+ step_number: int
19
+ step_title: str
20
+ content: str # Enriched with context
21
+ embedding: list[float] | None = None
22
+ metadata: dict = field(default_factory=dict)
23
+
24
+
25
+ @dataclass
26
+ class ChecklistChunk:
27
+ checklist_id: UUID
28
+ checklist_title: str
29
+ item_number: int
30
+ description: str
31
+ content: str
32
+ embedding: list[float] | None = None
33
+ metadata: dict = field(default_factory=dict)
34
+
35
+
36
+ @dataclass
37
+ class RunbookChunk:
38
+ runbook_id: UUID
39
+ runbook_title: str
40
+ step_number: int
41
+ action: str
42
+ content: str
43
+ embedding: list[float] | None = None
44
+ metadata: dict = field(default_factory=dict)
45
+
46
+
47
+ @dataclass
48
+ class GenericChunk:
49
+ node_id: UUID
50
+ node_title: str
51
+ chunk_index: int
52
+ content: str
53
+ embedding: list[float] | None = None
54
+ metadata: dict = field(default_factory=dict)
55
+
56
+
57
+ class ProceduralChunker:
58
+ """
59
+ Chunks SOPs, checklists, and runbooks in a step-aware way.
60
+ Standard token-based chunking destroys SOPs by splitting across chunks.
61
+ Each chunk contains:
62
+ - The step/item itself
63
+ - SOP/checklist title and purpose (always present)
64
+ - Previous step summary (so agents know what happened before)
65
+ - Next step preview (so agents know what comes after)
66
+ """
67
+
68
+ def __init__(self, max_chunk_tokens: int = 512):
69
+ self.max_chunk_tokens = max_chunk_tokens
70
+
71
+ def chunk_sop(self, sop: SOP) -> list[SOPChunk]:
72
+ chunks = []
73
+ steps = sop.steps
74
+ for i, step in enumerate(steps):
75
+ prev_summary = ""
76
+ if i > 0:
77
+ prev = steps[i - 1]
78
+ prev_summary = f"Previous step ({prev.step_number}. {prev.title}): {prev.description[:120]}..."
79
+
80
+ next_preview = ""
81
+ if i < len(steps) - 1:
82
+ nxt = steps[i + 1]
83
+ next_preview = f"Next step ({nxt.step_number}. {nxt.title}): {nxt.description[:80]}..."
84
+
85
+ tools_str = ", ".join(step.tools_required) if step.tools_required else "none"
86
+ role_str = step.responsible_role or "unspecified"
87
+
88
+ content = (
89
+ f"SOP: {sop.title}\n"
90
+ f"Purpose: {sop.purpose}\n"
91
+ f"Scope: {sop.scope}\n"
92
+ f"---\n"
93
+ f"Step {step.step_number} of {len(steps)}: {step.title}\n"
94
+ f"Description: {step.description}\n"
95
+ f"Responsible role: {role_str}\n"
96
+ f"Tools required: {tools_str}\n"
97
+ )
98
+ if step.decision_points:
99
+ content += f"Decision points: {', '.join(step.decision_points)}\n"
100
+ if step.on_failure:
101
+ content += f"On failure: {step.on_failure}\n"
102
+ if step.estimated_duration_minutes:
103
+ content += f"Estimated duration: {step.estimated_duration_minutes} minutes\n"
104
+ if prev_summary:
105
+ content += f"---\n{prev_summary}\n"
106
+ if next_preview:
107
+ content += f"{next_preview}\n"
108
+
109
+ # Handle substeps
110
+ if step.substeps:
111
+ content += "Substeps:\n"
112
+ for substep in step.substeps:
113
+ content += f" {substep.step_number}. {substep.title}: {substep.description}\n"
114
+
115
+ chunks.append(SOPChunk(
116
+ sop_id=sop.id,
117
+ sop_title=sop.title,
118
+ step_number=step.step_number,
119
+ step_title=step.title,
120
+ content=content,
121
+ metadata={
122
+ "org_id": sop.org_id,
123
+ "tags": sop.tags,
124
+ "roles_involved": sop.roles_involved,
125
+ "tools_required": sop.tools_required,
126
+ }
127
+ ))
128
+ return chunks
129
+
130
+ def chunk_checklist(self, checklist: Checklist) -> list[ChecklistChunk]:
131
+ chunks = []
132
+ items = checklist.items
133
+ for i, item in enumerate(items):
134
+ prev_summary = ""
135
+ if i > 0:
136
+ prev = items[i - 1]
137
+ prev_summary = f"Previous item ({prev.item_number}): {prev.description[:100]}..."
138
+
139
+ next_preview = ""
140
+ if i < len(items) - 1:
141
+ nxt = items[i + 1]
142
+ next_preview = f"Next item ({nxt.item_number}): {nxt.description[:80]}..."
143
+
144
+ content = (
145
+ f"Checklist: {checklist.title}\n"
146
+ f"Purpose: {checklist.purpose}\n"
147
+ f"---\n"
148
+ f"Item {item.item_number} of {len(items)}: {item.description}\n"
149
+ f"Required: {'Yes' if item.required else 'No'}\n"
150
+ )
151
+ if item.verification_method:
152
+ content += f"Verification: {item.verification_method}\n"
153
+ if item.responsible_role:
154
+ content += f"Responsible: {item.responsible_role}\n"
155
+ if item.time_estimate_minutes:
156
+ content += f"Time estimate: {item.time_estimate_minutes} minutes\n"
157
+ if item.notes:
158
+ content += f"Notes: {item.notes}\n"
159
+ if prev_summary:
160
+ content += f"---\n{prev_summary}\n"
161
+ if next_preview:
162
+ content += f"{next_preview}\n"
163
+
164
+ chunks.append(ChecklistChunk(
165
+ checklist_id=checklist.id,
166
+ checklist_title=checklist.title,
167
+ item_number=item.item_number,
168
+ description=item.description,
169
+ content=content,
170
+ metadata={
171
+ "org_id": checklist.org_id,
172
+ "tags": checklist.tags,
173
+ "frequency": checklist.frequency,
174
+ "trigger": checklist.trigger,
175
+ }
176
+ ))
177
+ return chunks
178
+
179
+ def chunk_runbook(self, runbook: Runbook) -> list[RunbookChunk]:
180
+ chunks = []
181
+ steps = runbook.steps
182
+ for i, step in enumerate(steps):
183
+ prev_summary = ""
184
+ if i > 0:
185
+ prev = steps[i - 1]
186
+ prev_summary = f"Previous step ({prev.step_number}): {prev.action[:100]}..."
187
+
188
+ next_preview = ""
189
+ if i < len(steps) - 1:
190
+ nxt = steps[i + 1]
191
+ next_preview = f"Next step ({nxt.step_number}): {nxt.action[:80]}..."
192
+
193
+ content = (
194
+ f"Runbook: {runbook.title}\n"
195
+ f"System: {runbook.system}\n"
196
+ f"Scenario: {runbook.scenario}\n"
197
+ f"Severity: {runbook.severity or 'unspecified'}\n"
198
+ f"---\n"
199
+ f"Step {step.step_number} of {len(steps)}: {step.action}\n"
200
+ )
201
+ if step.command:
202
+ content += f"Command: {step.command}\n"
203
+ if step.expected_output:
204
+ content += f"Expected output: {step.expected_output}\n"
205
+ if step.on_success:
206
+ content += f"On success: {step.on_success}\n"
207
+ if step.on_failure:
208
+ content += f"On failure: {step.on_failure}\n"
209
+ if step.rollback:
210
+ content += f"Rollback: {step.rollback}\n"
211
+ if prev_summary:
212
+ content += f"---\n{prev_summary}\n"
213
+ if next_preview:
214
+ content += f"{next_preview}\n"
215
+
216
+ chunks.append(RunbookChunk(
217
+ runbook_id=runbook.id,
218
+ runbook_title=runbook.title,
219
+ step_number=step.step_number,
220
+ action=step.action,
221
+ content=content,
222
+ metadata={
223
+ "org_id": runbook.org_id,
224
+ "tags": runbook.tags,
225
+ "system": runbook.system,
226
+ "severity": runbook.severity,
227
+ }
228
+ ))
229
+ return chunks
230
+
231
+ def chunk_generic(self, node: CivilizationNode, text: str,
232
+ overlap_tokens: int = 50) -> list[GenericChunk]:
233
+ """
234
+ Token-based chunking for non-procedural content.
235
+ Uses simple word-based approximation (1 token ~ 0.75 words).
236
+ """
237
+ words = text.split()
238
+ words_per_chunk = int(self.max_chunk_tokens * 0.75)
239
+ overlap_words = int(overlap_tokens * 0.75)
240
+
241
+ chunks = []
242
+ start = 0
243
+ chunk_index = 0
244
+
245
+ while start < len(words):
246
+ end = min(start + words_per_chunk, len(words))
247
+ chunk_text = " ".join(words[start:end])
248
+
249
+ # Add node context header
250
+ content = (
251
+ f"{node.node_type.value.upper()}: {node.title}\n"
252
+ f"---\n"
253
+ f"{chunk_text}"
254
+ )
255
+
256
+ chunks.append(GenericChunk(
257
+ node_id=node.id,
258
+ node_title=node.title,
259
+ chunk_index=chunk_index,
260
+ content=content,
261
+ metadata={
262
+ "org_id": node.org_id,
263
+ "node_type": node.node_type.value,
264
+ "tags": node.tags,
265
+ }
266
+ ))
267
+
268
+ if end >= len(words):
269
+ break
270
+ start = end - overlap_words
271
+ chunk_index += 1
272
+
273
+ return chunks