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,358 @@
1
+ """
2
+ Document ingestion pipeline for Civilization Knowledge Layer.
3
+ Orchestrates parsing, extraction, chunking, and embedding.
4
+ """
5
+ from __future__ import annotations
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, BinaryIO
10
+ from uuid import UUID, uuid4
11
+
12
+ from ..schema import (
13
+ CivilizationNode, NodeType, NodeStatus,
14
+ SOP, SOPStep, Checklist, ChecklistItem, Runbook, RunbookStep,
15
+ JobDescription, Role, Workflow, WorkflowNode, WorkflowEdge,
16
+ KnowledgeArticle, Policy,
17
+ )
18
+ from .chunker import ProceduralChunker, SOPChunk, ChecklistChunk, RunbookChunk, GenericChunk
19
+ from .extractor import MetadataExtractor, EntityExtractor, ExtractedMetadata
20
+ from .embedder import CivilizationEmbedder
21
+ from hyperclaw.taint import training_sink
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class IngestionResult:
28
+ """Result of document ingestion."""
29
+ success: bool
30
+ node_id: UUID | None = None
31
+ node_type: NodeType | None = None
32
+ chunks_created: int = 0
33
+ entities_extracted: int = 0
34
+ warnings: list[str] = field(default_factory=list)
35
+ errors: list[str] = field(default_factory=list)
36
+ metadata: dict = field(default_factory=dict)
37
+
38
+
39
+ class DocumentParser:
40
+ """Parse various document formats into plain text."""
41
+
42
+ SUPPORTED_EXTENSIONS = {".txt", ".md", ".markdown", ".rst", ".json", ".yaml", ".yml"}
43
+
44
+ def parse(self, content: str | bytes, filename: str | None = None) -> str:
45
+ """Parse document content to plain text."""
46
+ if isinstance(content, bytes):
47
+ content = content.decode("utf-8", errors="replace")
48
+
49
+ if filename:
50
+ ext = Path(filename).suffix.lower()
51
+ if ext in {".json"}:
52
+ return self._parse_json(content)
53
+ if ext in {".yaml", ".yml"}:
54
+ return self._parse_yaml(content)
55
+
56
+ return content
57
+
58
+ def _parse_json(self, content: str) -> str:
59
+ """Parse JSON to readable text."""
60
+ import json
61
+ try:
62
+ data = json.loads(content)
63
+ return self._dict_to_text(data)
64
+ except json.JSONDecodeError:
65
+ return content
66
+
67
+ def _parse_yaml(self, content: str) -> str:
68
+ """Parse YAML to readable text."""
69
+ try:
70
+ import yaml
71
+ data = yaml.safe_load(content)
72
+ return self._dict_to_text(data)
73
+ except Exception:
74
+ return content
75
+
76
+ def _dict_to_text(self, data: Any, indent: int = 0) -> str:
77
+ """Convert dict/list to readable text format."""
78
+ lines = []
79
+ prefix = " " * indent
80
+
81
+ if isinstance(data, dict):
82
+ for key, value in data.items():
83
+ if isinstance(value, (dict, list)):
84
+ lines.append(f"{prefix}{key}:")
85
+ lines.append(self._dict_to_text(value, indent + 1))
86
+ else:
87
+ lines.append(f"{prefix}{key}: {value}")
88
+ elif isinstance(data, list):
89
+ for item in data:
90
+ if isinstance(item, (dict, list)):
91
+ lines.append(f"{prefix}-")
92
+ lines.append(self._dict_to_text(item, indent + 1))
93
+ else:
94
+ lines.append(f"{prefix}- {item}")
95
+ else:
96
+ lines.append(f"{prefix}{data}")
97
+
98
+ return "\n".join(lines)
99
+
100
+
101
+ class DocumentIngestor:
102
+ """
103
+ Main ingestion pipeline for Civilization Knowledge Layer.
104
+ Coordinates parsing, extraction, chunking, and embedding.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ org_id: str,
110
+ embedder: CivilizationEmbedder | None = None,
111
+ chunker: ProceduralChunker | None = None,
112
+ store=None, # CivilizationStore
113
+ ):
114
+ self.org_id = org_id
115
+ self.embedder = embedder or CivilizationEmbedder()
116
+ self.chunker = chunker or ProceduralChunker()
117
+ self.store = store
118
+ self.parser = DocumentParser()
119
+ self.metadata_extractor = MetadataExtractor()
120
+ self.entity_extractor = EntityExtractor()
121
+
122
+ @training_sink("civilization.ingest")
123
+ async def ingest(
124
+ self,
125
+ content: str | bytes,
126
+ filename: str | None = None,
127
+ source: str | None = None,
128
+ source_url: str | None = None,
129
+ node_type_hint: NodeType | None = None,
130
+ owner_id: str | None = None,
131
+ tags: list[str] | None = None,
132
+ ) -> IngestionResult:
133
+ """
134
+ Ingest a document into the Civilization Knowledge Layer.
135
+
136
+ Args:
137
+ content: Raw document content (string or bytes)
138
+ filename: Original filename (helps with parsing)
139
+ source: Source system (e.g., "notion", "confluence", "upload")
140
+ source_url: URL of the source document
141
+ node_type_hint: Override auto-detection of node type
142
+ owner_id: ID of the document owner
143
+ tags: Tags to apply to the node
144
+
145
+ Returns:
146
+ IngestionResult with details of ingestion
147
+
148
+ Raises hyperclaw.taint.TaintedInput for connector content (a tainted
149
+ argument, or `source` naming a connector such as gdrive/notion).
150
+ """
151
+ from hyperclaw.taint import refuse_connector_source
152
+ refuse_connector_source("civilization.ingest", source)
153
+ result = IngestionResult(success=False)
154
+
155
+ try:
156
+ # Parse document
157
+ text = self.parser.parse(content, filename)
158
+ if not text.strip():
159
+ result.errors.append("Document is empty after parsing")
160
+ return result
161
+
162
+ # Extract metadata
163
+ metadata = self.metadata_extractor.extract(text)
164
+ result.metadata = {
165
+ "title": metadata.title,
166
+ "purpose": metadata.purpose,
167
+ "detected_type": metadata.document_type,
168
+ "complexity": metadata.estimated_complexity,
169
+ }
170
+
171
+ # Determine node type
172
+ node_type = node_type_hint
173
+ if not node_type:
174
+ node_type = self._detect_node_type(metadata)
175
+ result.node_type = node_type
176
+
177
+ # Extract entities
178
+ entities = self.entity_extractor.extract(text)
179
+ result.entities_extracted = len(entities)
180
+
181
+ # Create node based on type
182
+ node = await self._create_node(
183
+ text=text,
184
+ metadata=metadata,
185
+ node_type=node_type,
186
+ source=source,
187
+ source_url=source_url,
188
+ owner_id=owner_id,
189
+ tags=tags or [],
190
+ )
191
+ result.node_id = node.id
192
+
193
+ # Generate chunks and embeddings
194
+ chunks = await self._chunk_and_embed(node, text)
195
+ result.chunks_created = len(chunks)
196
+
197
+ # Save to store if available
198
+ if self.store:
199
+ await self.store.save(node)
200
+
201
+ result.success = True
202
+
203
+ except Exception as e:
204
+ logger.exception("Ingestion failed")
205
+ result.errors.append(str(e))
206
+
207
+ return result
208
+
209
+ def _detect_node_type(self, metadata: ExtractedMetadata) -> NodeType:
210
+ """Detect node type from extracted metadata."""
211
+ type_map = {
212
+ "sop": NodeType.SOP,
213
+ "checklist": NodeType.CHECKLIST,
214
+ "runbook": NodeType.RUNBOOK,
215
+ "job_description": NodeType.JOB_DESCRIPTION,
216
+ "org_chart": NodeType.ORG_CHART,
217
+ "workflow": NodeType.WORKFLOW,
218
+ "policy": NodeType.POLICY,
219
+ "knowledge_article": NodeType.KNOWLEDGE_ARTICLE,
220
+ }
221
+ return type_map.get(metadata.document_type or "", NodeType.KNOWLEDGE_ARTICLE)
222
+
223
+ async def _create_node(
224
+ self,
225
+ text: str,
226
+ metadata: ExtractedMetadata,
227
+ node_type: NodeType,
228
+ source: str | None,
229
+ source_url: str | None,
230
+ owner_id: str | None,
231
+ tags: list[str],
232
+ ) -> CivilizationNode:
233
+ """Create the appropriate node type."""
234
+ base_kwargs = {
235
+ "org_id": self.org_id,
236
+ "title": metadata.title or "Untitled Document",
237
+ "source": source,
238
+ "source_url": source_url,
239
+ "owner_id": owner_id,
240
+ "tags": tags + metadata.tags,
241
+ "status": NodeStatus.DRAFT,
242
+ }
243
+
244
+ # Generate embedding for the full document (or summary)
245
+ summary_text = text[:2000] if len(text) > 2000 else text
246
+ embedding = await self.embedder.embed(summary_text)
247
+ base_kwargs["embedding"] = embedding
248
+
249
+ if node_type == NodeType.SOP:
250
+ return SOP(
251
+ **base_kwargs,
252
+ purpose=metadata.purpose or "",
253
+ scope="",
254
+ steps=[SOPStep(step_number=1, title="Step 1", description=text[:500])],
255
+ roles_involved=metadata.roles_mentioned,
256
+ tools_required=metadata.tools_mentioned,
257
+ )
258
+ elif node_type == NodeType.CHECKLIST:
259
+ return Checklist(
260
+ **base_kwargs,
261
+ purpose=metadata.purpose or "",
262
+ items=[ChecklistItem(item_number=1, description=text[:500])],
263
+ roles_involved=metadata.roles_mentioned,
264
+ )
265
+ elif node_type == NodeType.RUNBOOK:
266
+ return Runbook(
267
+ **base_kwargs,
268
+ system=metadata.systems_mentioned[0] if metadata.systems_mentioned else "Unknown",
269
+ scenario="",
270
+ steps=[RunbookStep(step_number=1, action=text[:500])],
271
+ )
272
+ elif node_type == NodeType.JOB_DESCRIPTION:
273
+ return JobDescription(
274
+ **base_kwargs,
275
+ role_title=metadata.title or "Unknown Role",
276
+ department=metadata.department or "",
277
+ summary=metadata.purpose or "",
278
+ responsibilities=[],
279
+ required_skills=[],
280
+ tools_used=metadata.tools_mentioned,
281
+ )
282
+ elif node_type == NodeType.WORKFLOW:
283
+ return Workflow(
284
+ **base_kwargs,
285
+ description=metadata.purpose or "",
286
+ nodes=[WorkflowNode(node_id="start", label="Start", node_type="start")],
287
+ edges=[],
288
+ )
289
+ elif node_type == NodeType.POLICY:
290
+ return Policy(
291
+ **base_kwargs,
292
+ policy_name=metadata.title or "Untitled Policy",
293
+ category="general",
294
+ summary=metadata.purpose or "",
295
+ full_text=text,
296
+ )
297
+ else:
298
+ return KnowledgeArticle(
299
+ **base_kwargs,
300
+ topic=metadata.title or "Unknown Topic",
301
+ category="general",
302
+ summary=metadata.purpose or text[:300],
303
+ content=text,
304
+ )
305
+
306
+ async def _chunk_and_embed(
307
+ self,
308
+ node: CivilizationNode,
309
+ text: str,
310
+ ) -> list[Any]:
311
+ """Create chunks and generate embeddings for each."""
312
+ chunks: list[Any] = []
313
+
314
+ if isinstance(node, SOP):
315
+ chunks = self.chunker.chunk_sop(node)
316
+ elif isinstance(node, Checklist):
317
+ chunks = self.chunker.chunk_checklist(node)
318
+ elif isinstance(node, Runbook):
319
+ chunks = self.chunker.chunk_runbook(node)
320
+ else:
321
+ chunks = self.chunker.chunk_generic(node, text)
322
+
323
+ # Generate embeddings for all chunks
324
+ if chunks:
325
+ contents = [c.content for c in chunks]
326
+ embeddings = await self.embedder.embed_batch(contents)
327
+ for chunk, embedding in zip(chunks, embeddings):
328
+ chunk.embedding = embedding
329
+
330
+ return chunks
331
+
332
+ @training_sink("civilization.ingest_batch")
333
+ async def ingest_batch(
334
+ self,
335
+ documents: list[dict],
336
+ ) -> list[IngestionResult]:
337
+ """
338
+ Ingest multiple documents.
339
+
340
+ Args:
341
+ documents: List of dicts with keys: content, filename, source, source_url, etc.
342
+
343
+ Returns:
344
+ List of IngestionResult for each document
345
+ """
346
+ results = []
347
+ for doc in documents:
348
+ result = await self.ingest(
349
+ content=doc.get("content", ""),
350
+ filename=doc.get("filename"),
351
+ source=doc.get("source"),
352
+ source_url=doc.get("source_url"),
353
+ node_type_hint=doc.get("node_type_hint"),
354
+ owner_id=doc.get("owner_id"),
355
+ tags=doc.get("tags"),
356
+ )
357
+ results.append(result)
358
+ return results
@@ -0,0 +1,262 @@
1
+ """
2
+ Embedding generation for Civilization Knowledge Layer.
3
+ Supports multiple embedding providers with a unified interface.
4
+ """
5
+ from __future__ import annotations
6
+ import os
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any
10
+ import hashlib
11
+ import json
12
+
13
+ from hyperclaw.taint import training_sink
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class EmbeddingProvider(ABC):
19
+ """Abstract base for embedding providers."""
20
+
21
+ @abstractmethod
22
+ async def embed(self, text: str) -> list[float]:
23
+ """Generate embedding for a single text."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
28
+ """Generate embeddings for multiple texts."""
29
+ pass
30
+
31
+ @property
32
+ @abstractmethod
33
+ def dimension(self) -> int:
34
+ """Return the embedding dimension."""
35
+ pass
36
+
37
+
38
+ class OpenAIEmbedder(EmbeddingProvider):
39
+ """OpenAI embedding provider using text-embedding-3-small."""
40
+
41
+ def __init__(self, api_key: str | None = None, model: str = "text-embedding-3-small"):
42
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
43
+ self.model = model
44
+ self._dimension = 1536 if "small" in model else 3072
45
+
46
+ async def embed(self, text: str) -> list[float]:
47
+ """Generate embedding for a single text using OpenAI."""
48
+ import httpx
49
+
50
+ async with httpx.AsyncClient() as client:
51
+ response = await client.post(
52
+ "https://api.openai.com/v1/embeddings",
53
+ headers={"Authorization": f"Bearer {self.api_key}"},
54
+ json={"input": text, "model": self.model},
55
+ timeout=30.0,
56
+ )
57
+ response.raise_for_status()
58
+ data = response.json()
59
+ return data["data"][0]["embedding"]
60
+
61
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
62
+ """Generate embeddings for multiple texts."""
63
+ import httpx
64
+
65
+ # OpenAI supports batches of up to 2048 inputs
66
+ async with httpx.AsyncClient() as client:
67
+ response = await client.post(
68
+ "https://api.openai.com/v1/embeddings",
69
+ headers={"Authorization": f"Bearer {self.api_key}"},
70
+ json={"input": texts, "model": self.model},
71
+ timeout=60.0,
72
+ )
73
+ response.raise_for_status()
74
+ data = response.json()
75
+ # Sort by index to maintain order
76
+ sorted_data = sorted(data["data"], key=lambda x: x["index"])
77
+ return [item["embedding"] for item in sorted_data]
78
+
79
+ @property
80
+ def dimension(self) -> int:
81
+ return self._dimension
82
+
83
+
84
+ class VoyageEmbedder(EmbeddingProvider):
85
+ """Voyage AI embedding provider."""
86
+
87
+ def __init__(self, api_key: str | None = None, model: str = "voyage-2"):
88
+ self.api_key = api_key or os.environ.get("VOYAGE_API_KEY")
89
+ self.model = model
90
+ self._dimension = 1024
91
+
92
+ async def embed(self, text: str) -> list[float]:
93
+ import httpx
94
+
95
+ async with httpx.AsyncClient() as client:
96
+ response = await client.post(
97
+ "https://api.voyageai.com/v1/embeddings",
98
+ headers={"Authorization": f"Bearer {self.api_key}"},
99
+ json={"input": text, "model": self.model},
100
+ timeout=30.0,
101
+ )
102
+ response.raise_for_status()
103
+ data = response.json()
104
+ return data["data"][0]["embedding"]
105
+
106
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
107
+ import httpx
108
+
109
+ async with httpx.AsyncClient() as client:
110
+ response = await client.post(
111
+ "https://api.voyageai.com/v1/embeddings",
112
+ headers={"Authorization": f"Bearer {self.api_key}"},
113
+ json={"input": texts, "model": self.model},
114
+ timeout=60.0,
115
+ )
116
+ response.raise_for_status()
117
+ data = response.json()
118
+ sorted_data = sorted(data["data"], key=lambda x: x["index"])
119
+ return [item["embedding"] for item in sorted_data]
120
+
121
+ @property
122
+ def dimension(self) -> int:
123
+ return self._dimension
124
+
125
+
126
+ class BedrockEmbedder(EmbeddingProvider):
127
+ """Amazon Bedrock Titan Embeddings V2."""
128
+
129
+ def __init__(self, model: str = "amazon.titan-embed-text-v2:0"):
130
+ import boto3
131
+ self.model = model
132
+ self._dimension = 1024
133
+ self._client = boto3.client("bedrock-runtime")
134
+
135
+ async def embed(self, text: str) -> list[float]:
136
+ response = self._client.invoke_model(
137
+ modelId=self.model,
138
+ body=json.dumps({"inputText": text, "dimensions": self._dimension}),
139
+ contentType="application/json",
140
+ accept="application/json",
141
+ )
142
+ data = json.loads(response["body"].read())
143
+ return data["embedding"]
144
+
145
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
146
+ # Titan V2 invoke_model is single-text only — no batch endpoint exists for this model
147
+ return [await self.embed(text) for text in texts]
148
+
149
+ @property
150
+ def dimension(self) -> int:
151
+ return self._dimension
152
+
153
+
154
+ class MockEmbedder(EmbeddingProvider):
155
+ """Mock embedder for testing. Generates deterministic embeddings based on text hash."""
156
+
157
+ def __init__(self, dimension: int = 1536):
158
+ self._dimension = dimension
159
+
160
+ async def embed(self, text: str) -> list[float]:
161
+ """Generate deterministic mock embedding from text hash."""
162
+ hash_bytes = hashlib.sha256(text.encode()).digest()
163
+ # Expand hash to fill dimension
164
+ embedding = []
165
+ for i in range(self._dimension):
166
+ byte_idx = i % len(hash_bytes)
167
+ # Normalize to [-1, 1]
168
+ value = (hash_bytes[byte_idx] / 127.5) - 1.0
169
+ embedding.append(value)
170
+ return embedding
171
+
172
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
173
+ return [await self.embed(text) for text in texts]
174
+
175
+ @property
176
+ def dimension(self) -> int:
177
+ return self._dimension
178
+
179
+
180
+ class CivilizationEmbedder:
181
+ """
182
+ Main embedder class for Civilization Knowledge Layer.
183
+ Handles caching, batching, and provider selection.
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ provider: EmbeddingProvider | None = None,
189
+ cache_enabled: bool = True,
190
+ ):
191
+ self.provider = provider or MockEmbedder()
192
+ self.cache_enabled = cache_enabled
193
+ self._cache: dict[str, list[float]] = {}
194
+
195
+ def _cache_key(self, text: str) -> str:
196
+ """Generate cache key for text."""
197
+ return hashlib.sha256(text.encode()).hexdigest()[:32]
198
+
199
+ @training_sink("civilization.embedder.embed", own_words=True)
200
+ async def embed(self, text: str) -> list[float]:
201
+ """Generate embedding for text with caching."""
202
+ if self.cache_enabled:
203
+ key = self._cache_key(text)
204
+ if key in self._cache:
205
+ return self._cache[key]
206
+
207
+ embedding = await self.provider.embed(text)
208
+
209
+ if self.cache_enabled:
210
+ self._cache[key] = embedding
211
+
212
+ return embedding
213
+
214
+ @training_sink("civilization.embedder.embed_batch", own_words=True)
215
+ async def embed_batch(self, texts: list[str]) -> list[list[float]]:
216
+ """Generate embeddings for multiple texts with caching."""
217
+ if not self.cache_enabled:
218
+ return await self.provider.embed_batch(texts)
219
+
220
+ # Check cache for existing embeddings
221
+ results: list[list[float] | None] = [None] * len(texts)
222
+ texts_to_embed: list[tuple[int, str]] = []
223
+
224
+ for i, text in enumerate(texts):
225
+ key = self._cache_key(text)
226
+ if key in self._cache:
227
+ results[i] = self._cache[key]
228
+ else:
229
+ texts_to_embed.append((i, text))
230
+
231
+ # Embed uncached texts
232
+ if texts_to_embed:
233
+ indices, uncached_texts = zip(*texts_to_embed)
234
+ new_embeddings = await self.provider.embed_batch(list(uncached_texts))
235
+
236
+ for idx, embedding, text in zip(indices, new_embeddings, uncached_texts):
237
+ results[idx] = embedding
238
+ self._cache[self._cache_key(text)] = embedding
239
+
240
+ return results # type: ignore
241
+
242
+ @property
243
+ def dimension(self) -> int:
244
+ """Return embedding dimension from provider."""
245
+ return self.provider.dimension
246
+
247
+ def clear_cache(self) -> None:
248
+ """Clear the embedding cache."""
249
+ self._cache.clear()
250
+
251
+ @classmethod
252
+ def create(cls, provider_name: str = "mock", **kwargs) -> "CivilizationEmbedder":
253
+ """Factory method to create embedder with specified provider."""
254
+ providers = {
255
+ "openai": OpenAIEmbedder,
256
+ "voyage": VoyageEmbedder,
257
+ "mock": MockEmbedder,
258
+ "bedrock": BedrockEmbedder,
259
+ }
260
+ provider_cls = providers.get(provider_name, MockEmbedder)
261
+ provider = provider_cls(**kwargs)
262
+ return cls(provider=provider)