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,17 @@
1
+ """
2
+ Retrieval module for Civilization Knowledge Layer.
3
+ Handles RAG, context injection, and relevance ranking.
4
+ """
5
+ from .civilization_rag import CivilizationRAG, RAGResult, RAGConfig
6
+ from .context_injector import ContextInjector, InjectionMode
7
+ from .relevance_ranker import RelevanceRanker, RankingResult
8
+
9
+ __all__ = [
10
+ "CivilizationRAG",
11
+ "RAGResult",
12
+ "RAGConfig",
13
+ "ContextInjector",
14
+ "InjectionMode",
15
+ "RelevanceRanker",
16
+ "RankingResult",
17
+ ]
@@ -0,0 +1,316 @@
1
+ """
2
+ RAG (Retrieval Augmented Generation) for Civilization Knowledge Layer.
3
+ Retrieves and formats organizational knowledge for agent consumption.
4
+ """
5
+ from __future__ import annotations
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Callable
9
+ from uuid import UUID
10
+
11
+ from ..schema import CivilizationNode, NodeType
12
+ from ..store import CivilizationStore
13
+ from ..ingestion.embedder import CivilizationEmbedder
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class RAGConfig:
20
+ """Configuration for RAG retrieval."""
21
+ top_k: int = 5
22
+ min_relevance_score: float = 0.5
23
+ include_metadata: bool = True
24
+ max_context_tokens: int = 4000
25
+ node_type_filters: list[NodeType] | None = None
26
+ tag_filters: list[str] | None = None
27
+ rerank: bool = True
28
+ expand_related: bool = True
29
+
30
+
31
+ @dataclass
32
+ class RetrievedNode:
33
+ """A node retrieved by RAG with relevance metadata."""
34
+ node: CivilizationNode
35
+ score: float
36
+ source: str = "vector" # "vector", "keyword", "related"
37
+ highlights: list[str] = field(default_factory=list)
38
+
39
+
40
+ @dataclass
41
+ class RAGResult:
42
+ """Result of a RAG query."""
43
+ query: str
44
+ retrieved_nodes: list[RetrievedNode] = field(default_factory=list)
45
+ context: str = ""
46
+ metadata: dict = field(default_factory=dict)
47
+
48
+ @property
49
+ def node_count(self) -> int:
50
+ return len(self.retrieved_nodes)
51
+
52
+ @property
53
+ def has_results(self) -> bool:
54
+ return len(self.retrieved_nodes) > 0
55
+
56
+ @property
57
+ def top_node(self) -> CivilizationNode | None:
58
+ if self.retrieved_nodes:
59
+ return self.retrieved_nodes[0].node
60
+ return None
61
+
62
+
63
+ class CivilizationRAG:
64
+ """
65
+ RAG system for retrieving and formatting organizational knowledge.
66
+ Supports vector search, keyword fallback, and context expansion.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ org_id: str,
72
+ store: CivilizationStore | None = None,
73
+ embedder: CivilizationEmbedder | None = None,
74
+ config: RAGConfig | None = None,
75
+ ):
76
+ self.org_id = org_id
77
+ self.store = store
78
+ self.embedder = embedder or CivilizationEmbedder()
79
+ self.config = config or RAGConfig()
80
+ self._cache: dict[str, list[RetrievedNode]] = {}
81
+
82
+ async def query(
83
+ self,
84
+ query: str,
85
+ config_override: RAGConfig | None = None,
86
+ ) -> RAGResult:
87
+ """
88
+ Query the knowledge base and retrieve relevant nodes.
89
+
90
+ Args:
91
+ query: The search query
92
+ config_override: Optional config to override defaults
93
+
94
+ Returns:
95
+ RAGResult with retrieved nodes and formatted context
96
+ """
97
+ config = config_override or self.config
98
+ result = RAGResult(query=query)
99
+
100
+ # Generate query embedding
101
+ query_embedding = await self.embedder.embed(query)
102
+
103
+ # Vector search
104
+ if self.store:
105
+ vector_results = await self.store.search_by_embedding(
106
+ org_id=self.org_id,
107
+ embedding=query_embedding,
108
+ top_k=config.top_k * 2, # Over-fetch for reranking
109
+ node_types=config.node_type_filters,
110
+ )
111
+
112
+ for row in vector_results:
113
+ score = row.get("score", 0.0)
114
+ if score >= config.min_relevance_score:
115
+ node = self.store._row_to_node(row)
116
+ result.retrieved_nodes.append(RetrievedNode(
117
+ node=node,
118
+ score=score,
119
+ source="vector",
120
+ ))
121
+
122
+ # Sort by score
123
+ result.retrieved_nodes.sort(key=lambda x: x.score, reverse=True)
124
+
125
+ # Truncate to top_k
126
+ result.retrieved_nodes = result.retrieved_nodes[:config.top_k]
127
+
128
+ # Expand with related nodes if enabled
129
+ if config.expand_related and result.retrieved_nodes:
130
+ await self._expand_related(result, config)
131
+
132
+ # Generate context string
133
+ result.context = self._format_context(result.retrieved_nodes, config)
134
+
135
+ result.metadata = {
136
+ "total_retrieved": len(result.retrieved_nodes),
137
+ "config": {
138
+ "top_k": config.top_k,
139
+ "min_score": config.min_relevance_score,
140
+ },
141
+ }
142
+
143
+ return result
144
+
145
+ async def query_by_type(
146
+ self,
147
+ query: str,
148
+ node_type: NodeType,
149
+ top_k: int = 5,
150
+ ) -> RAGResult:
151
+ """Query for a specific node type."""
152
+ config = RAGConfig(
153
+ top_k=top_k,
154
+ node_type_filters=[node_type],
155
+ )
156
+ return await self.query(query, config)
157
+
158
+ async def get_sop_for_task(self, task_description: str) -> RAGResult:
159
+ """Find relevant SOPs for a task."""
160
+ return await self.query_by_type(
161
+ query=f"SOP procedure for: {task_description}",
162
+ node_type=NodeType.SOP,
163
+ top_k=3,
164
+ )
165
+
166
+ async def get_role_info(self, role_query: str) -> RAGResult:
167
+ """Find role definitions."""
168
+ config = RAGConfig(
169
+ top_k=3,
170
+ node_type_filters=[NodeType.ROLE, NodeType.JOB_DESCRIPTION],
171
+ )
172
+ return await self.query(role_query, config)
173
+
174
+ async def get_runbook_for_incident(self, incident_description: str) -> RAGResult:
175
+ """Find relevant runbooks for an incident."""
176
+ return await self.query_by_type(
177
+ query=f"Runbook for incident: {incident_description}",
178
+ node_type=NodeType.RUNBOOK,
179
+ top_k=3,
180
+ )
181
+
182
+ async def _expand_related(self, result: RAGResult, config: RAGConfig) -> None:
183
+ """Expand results with related nodes."""
184
+ if not self.store:
185
+ return
186
+
187
+ # Get IDs of related nodes from top results
188
+ related_ids: set[UUID] = set()
189
+ for rn in result.retrieved_nodes[:3]:
190
+ # Check for related_sops, related_roles, etc.
191
+ node_dict = rn.node.model_dump()
192
+ for key, value in node_dict.items():
193
+ if key.startswith("related_") and isinstance(value, list):
194
+ for item in value:
195
+ if isinstance(item, (UUID, str)):
196
+ try:
197
+ related_ids.add(UUID(str(item)))
198
+ except ValueError:
199
+ pass
200
+
201
+ # Fetch and add related nodes
202
+ existing_ids = {rn.node.id for rn in result.retrieved_nodes}
203
+ for rel_id in related_ids:
204
+ if rel_id not in existing_ids and len(result.retrieved_nodes) < config.top_k + 2:
205
+ related = await self.store.get(rel_id, self.org_id)
206
+ if related:
207
+ result.retrieved_nodes.append(RetrievedNode(
208
+ node=related,
209
+ score=0.5, # Default score for related
210
+ source="related",
211
+ ))
212
+
213
+ def _format_context(
214
+ self,
215
+ nodes: list[RetrievedNode],
216
+ config: RAGConfig,
217
+ ) -> str:
218
+ """Format retrieved nodes into context string for LLM."""
219
+ if not nodes:
220
+ return ""
221
+
222
+ parts = []
223
+ token_estimate = 0
224
+ max_tokens = config.max_context_tokens
225
+
226
+ for i, rn in enumerate(nodes, 1):
227
+ node = rn.node
228
+ section = self._format_node(node, i, config.include_metadata)
229
+
230
+ # Rough token estimate (4 chars per token)
231
+ section_tokens = len(section) // 4
232
+ if token_estimate + section_tokens > max_tokens:
233
+ break
234
+
235
+ parts.append(section)
236
+ token_estimate += section_tokens
237
+
238
+ return "\n\n---\n\n".join(parts)
239
+
240
+ def _format_node(
241
+ self,
242
+ node: CivilizationNode,
243
+ index: int,
244
+ include_metadata: bool,
245
+ ) -> str:
246
+ """Format a single node for context."""
247
+ lines = [f"[{index}] {node.node_type.value.upper()}: {node.title}"]
248
+
249
+ if include_metadata:
250
+ if node.owner_id:
251
+ lines.append(f"Owner: {node.owner_id}")
252
+ if node.tags:
253
+ lines.append(f"Tags: {', '.join(node.tags)}")
254
+
255
+ # Type-specific formatting
256
+ node_dict = node.model_dump()
257
+
258
+ if node.node_type == NodeType.SOP:
259
+ if "purpose" in node_dict:
260
+ lines.append(f"Purpose: {node_dict['purpose']}")
261
+ if "steps" in node_dict:
262
+ lines.append("Steps:")
263
+ for step in node_dict["steps"][:5]:
264
+ lines.append(f" {step.get('step_number', '?')}. {step.get('title', 'Untitled')}")
265
+ if step.get("description"):
266
+ lines.append(f" {step['description'][:150]}...")
267
+
268
+ elif node.node_type == NodeType.ROLE:
269
+ if "role_title" in node_dict:
270
+ lines.append(f"Title: {node_dict['role_title']}")
271
+ if "responsibilities" in node_dict:
272
+ lines.append("Responsibilities:")
273
+ for r in node_dict["responsibilities"][:5]:
274
+ lines.append(f" - {r}")
275
+
276
+ elif node.node_type == NodeType.RUNBOOK:
277
+ if "scenario" in node_dict:
278
+ lines.append(f"Scenario: {node_dict['scenario']}")
279
+ if "steps" in node_dict:
280
+ lines.append("Steps:")
281
+ for step in node_dict["steps"][:5]:
282
+ lines.append(f" {step.get('step_number', '?')}. {step.get('action', 'Action')}")
283
+
284
+ elif node.node_type == NodeType.CHECKLIST:
285
+ if "items" in node_dict:
286
+ lines.append("Items:")
287
+ for item in node_dict["items"][:8]:
288
+ lines.append(f" [ ] {item.get('description', '')}")
289
+
290
+ return "\n".join(lines)
291
+
292
+ async def hybrid_search(
293
+ self,
294
+ query: str,
295
+ keyword_weight: float = 0.3,
296
+ ) -> RAGResult:
297
+ """
298
+ Perform hybrid search combining vector and keyword matching.
299
+ """
300
+ # Vector search
301
+ vector_result = await self.query(query)
302
+
303
+ # For keyword search, we'd need text search capability in the store
304
+ # This is a placeholder that could be enhanced
305
+ result = RAGResult(
306
+ query=query,
307
+ retrieved_nodes=vector_result.retrieved_nodes,
308
+ metadata={"search_type": "hybrid"},
309
+ )
310
+
311
+ result.context = self._format_context(result.retrieved_nodes, self.config)
312
+ return result
313
+
314
+ def clear_cache(self) -> None:
315
+ """Clear the query cache."""
316
+ self._cache.clear()
@@ -0,0 +1,342 @@
1
+ """
2
+ Context injection for agent prompts.
3
+ Formats and injects organizational knowledge into agent context.
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
+
11
+ from ..schema import CivilizationNode, NodeType
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class InjectionMode(str, Enum):
17
+ FULL = "full" # Include complete node content
18
+ SUMMARY = "summary" # Include only summaries
19
+ REFERENCE = "reference" # Include only titles and IDs
20
+ STRUCTURED = "structured" # Include structured data format
21
+ MINIMAL = "minimal" # Bare minimum context
22
+
23
+
24
+ @dataclass
25
+ class InjectionConfig:
26
+ """Configuration for context injection."""
27
+ mode: InjectionMode = InjectionMode.SUMMARY
28
+ max_tokens: int = 2000
29
+ include_metadata: bool = True
30
+ include_relationships: bool = True
31
+ priority_types: list[NodeType] | None = None
32
+ format: str = "markdown" # "markdown", "xml", "json"
33
+
34
+
35
+ @dataclass
36
+ class InjectionResult:
37
+ """Result of context injection."""
38
+ context: str
39
+ nodes_included: int
40
+ truncated: bool = False
41
+ token_estimate: int = 0
42
+
43
+
44
+ class ContextInjector:
45
+ """
46
+ Injects civilization knowledge into agent prompts.
47
+ Handles formatting, token budgeting, and prioritization.
48
+ """
49
+
50
+ PRIORITY_ORDER = [
51
+ NodeType.SOP,
52
+ NodeType.RUNBOOK,
53
+ NodeType.CHECKLIST,
54
+ NodeType.ROLE,
55
+ NodeType.WORKFLOW,
56
+ NodeType.POLICY,
57
+ NodeType.JOB_DESCRIPTION,
58
+ NodeType.CLIENT_PROFILE,
59
+ NodeType.KNOWLEDGE_ARTICLE,
60
+ NodeType.ORG_CHART,
61
+ NodeType.PERSON,
62
+ NodeType.PERSONAL_ROUTINE,
63
+ ]
64
+
65
+ def __init__(self, config: InjectionConfig | None = None):
66
+ self.config = config or InjectionConfig()
67
+
68
+ def inject(
69
+ self,
70
+ nodes: list[CivilizationNode],
71
+ config_override: InjectionConfig | None = None,
72
+ ) -> InjectionResult:
73
+ """
74
+ Generate injection context from nodes.
75
+
76
+ Args:
77
+ nodes: Nodes to include in context
78
+ config_override: Optional config override
79
+
80
+ Returns:
81
+ InjectionResult with formatted context
82
+ """
83
+ config = config_override or self.config
84
+ result = InjectionResult(context="", nodes_included=0)
85
+
86
+ if not nodes:
87
+ return result
88
+
89
+ # Sort by priority
90
+ sorted_nodes = self._prioritize_nodes(nodes, config)
91
+
92
+ # Format based on mode
93
+ formatter = self._get_formatter(config.format)
94
+ sections = []
95
+ total_tokens = 0
96
+
97
+ for node in sorted_nodes:
98
+ section = self._format_node(node, config)
99
+ section_tokens = len(section) // 4 # Rough estimate
100
+
101
+ if total_tokens + section_tokens > config.max_tokens:
102
+ result.truncated = True
103
+ break
104
+
105
+ sections.append(section)
106
+ total_tokens += section_tokens
107
+ result.nodes_included += 1
108
+
109
+ result.context = formatter(sections, config)
110
+ result.token_estimate = total_tokens
111
+
112
+ return result
113
+
114
+ def _prioritize_nodes(
115
+ self,
116
+ nodes: list[CivilizationNode],
117
+ config: InjectionConfig,
118
+ ) -> list[CivilizationNode]:
119
+ """Sort nodes by priority."""
120
+ priority_types = config.priority_types or self.PRIORITY_ORDER
121
+
122
+ def sort_key(node: CivilizationNode) -> tuple:
123
+ try:
124
+ type_priority = priority_types.index(node.node_type)
125
+ except ValueError:
126
+ type_priority = len(priority_types)
127
+ return (type_priority, -node.updated_at.timestamp())
128
+
129
+ return sorted(nodes, key=sort_key)
130
+
131
+ def _format_node(
132
+ self,
133
+ node: CivilizationNode,
134
+ config: InjectionConfig,
135
+ ) -> str:
136
+ """Format a single node based on injection mode."""
137
+ if config.mode == InjectionMode.REFERENCE:
138
+ return self._format_reference(node)
139
+ elif config.mode == InjectionMode.MINIMAL:
140
+ return self._format_minimal(node)
141
+ elif config.mode == InjectionMode.SUMMARY:
142
+ return self._format_summary(node, config)
143
+ elif config.mode == InjectionMode.STRUCTURED:
144
+ return self._format_structured(node, config)
145
+ else: # FULL
146
+ return self._format_full(node, config)
147
+
148
+ def _format_reference(self, node: CivilizationNode) -> str:
149
+ """Format as reference only."""
150
+ return f"- [{node.node_type.value}] {node.title} (ID: {node.id})"
151
+
152
+ def _format_minimal(self, node: CivilizationNode) -> str:
153
+ """Format with minimal information."""
154
+ return f"{node.node_type.value.upper()}: {node.title}"
155
+
156
+ def _format_summary(
157
+ self,
158
+ node: CivilizationNode,
159
+ config: InjectionConfig,
160
+ ) -> str:
161
+ """Format as summary."""
162
+ lines = [f"**{node.node_type.value.upper()}**: {node.title}"]
163
+
164
+ node_dict = node.model_dump()
165
+
166
+ # Add key fields based on node type
167
+ if node.node_type == NodeType.SOP and "purpose" in node_dict:
168
+ lines.append(f"Purpose: {node_dict['purpose']}")
169
+ if "steps" in node_dict:
170
+ step_count = len(node_dict["steps"])
171
+ lines.append(f"Steps: {step_count} total")
172
+
173
+ elif node.node_type == NodeType.ROLE:
174
+ if "role_title" in node_dict:
175
+ lines.append(f"Role: {node_dict['role_title']}")
176
+ if "responsibilities" in node_dict:
177
+ lines.append(f"Key responsibilities: {len(node_dict['responsibilities'])} defined")
178
+
179
+ elif node.node_type == NodeType.RUNBOOK:
180
+ if "scenario" in node_dict:
181
+ lines.append(f"Scenario: {node_dict['scenario']}")
182
+ if "severity" in node_dict:
183
+ lines.append(f"Severity: {node_dict['severity']}")
184
+
185
+ if config.include_metadata and node.tags:
186
+ lines.append(f"Tags: {', '.join(node.tags[:5])}")
187
+
188
+ return "\n".join(lines)
189
+
190
+ def _format_structured(
191
+ self,
192
+ node: CivilizationNode,
193
+ config: InjectionConfig,
194
+ ) -> str:
195
+ """Format as structured data."""
196
+ import json
197
+ data = {
198
+ "type": node.node_type.value,
199
+ "title": node.title,
200
+ "id": str(node.id),
201
+ }
202
+
203
+ node_dict = node.model_dump()
204
+
205
+ # Include type-specific fields
206
+ if node.node_type == NodeType.SOP:
207
+ data["purpose"] = node_dict.get("purpose", "")
208
+ data["step_count"] = len(node_dict.get("steps", []))
209
+ data["roles"] = node_dict.get("roles_involved", [])
210
+
211
+ elif node.node_type == NodeType.ROLE:
212
+ data["role_title"] = node_dict.get("role_title", "")
213
+ data["department"] = node_dict.get("department", "")
214
+
215
+ if config.include_metadata:
216
+ data["tags"] = node.tags
217
+ data["owner"] = node.owner_id
218
+
219
+ return json.dumps(data, indent=2)
220
+
221
+ def _format_full(
222
+ self,
223
+ node: CivilizationNode,
224
+ config: InjectionConfig,
225
+ ) -> str:
226
+ """Format with full content."""
227
+ lines = [
228
+ f"## {node.node_type.value.upper()}: {node.title}",
229
+ "",
230
+ ]
231
+
232
+ node_dict = node.model_dump()
233
+
234
+ # Full content varies by type
235
+ if node.node_type == NodeType.SOP:
236
+ lines.append(f"**Purpose:** {node_dict.get('purpose', 'N/A')}")
237
+ lines.append(f"**Scope:** {node_dict.get('scope', 'N/A')}")
238
+ lines.append("")
239
+
240
+ if "steps" in node_dict:
241
+ lines.append("### Steps")
242
+ for step in node_dict["steps"]:
243
+ lines.append(f"{step.get('step_number', '?')}. **{step.get('title', 'Step')}**")
244
+ lines.append(f" {step.get('description', '')}")
245
+ if step.get("responsible_role"):
246
+ lines.append(f" - Role: {step['responsible_role']}")
247
+ if step.get("tools_required"):
248
+ lines.append(f" - Tools: {', '.join(step['tools_required'])}")
249
+ lines.append("")
250
+
251
+ elif node.node_type == NodeType.ROLE:
252
+ lines.append(f"**Title:** {node_dict.get('role_title', 'N/A')}")
253
+ lines.append(f"**Department:** {node_dict.get('department', 'N/A')}")
254
+ lines.append("")
255
+
256
+ if "responsibilities" in node_dict:
257
+ lines.append("### Responsibilities")
258
+ for r in node_dict["responsibilities"]:
259
+ lines.append(f"- {r}")
260
+
261
+ if "accountabilities" in node_dict:
262
+ lines.append("")
263
+ lines.append("### Accountabilities")
264
+ for a in node_dict["accountabilities"]:
265
+ lines.append(f"- {a}")
266
+
267
+ elif node.node_type == NodeType.CHECKLIST:
268
+ lines.append(f"**Purpose:** {node_dict.get('purpose', 'N/A')}")
269
+ lines.append("")
270
+
271
+ if "items" in node_dict:
272
+ lines.append("### Items")
273
+ for item in node_dict["items"]:
274
+ req = "[Required]" if item.get("required") else "[Optional]"
275
+ lines.append(f"- {req} {item.get('description', '')}")
276
+
277
+ if config.include_metadata:
278
+ lines.append("")
279
+ lines.append("---")
280
+ lines.append(f"*Owner: {node.owner_id or 'Unassigned'}*")
281
+ lines.append(f"*Tags: {', '.join(node.tags) if node.tags else 'None'}*")
282
+ lines.append(f"*Last updated: {node.updated_at.isoformat()}*")
283
+
284
+ return "\n".join(lines)
285
+
286
+ def _get_formatter(self, format_type: str):
287
+ """Get formatter function for output format."""
288
+ if format_type == "xml":
289
+ return self._format_as_xml
290
+ elif format_type == "json":
291
+ return self._format_as_json
292
+ else: # markdown
293
+ return self._format_as_markdown
294
+
295
+ def _format_as_markdown(
296
+ self,
297
+ sections: list[str],
298
+ config: InjectionConfig,
299
+ ) -> str:
300
+ """Format sections as markdown."""
301
+ header = "# Organizational Knowledge Context\n\n"
302
+ return header + "\n\n---\n\n".join(sections)
303
+
304
+ def _format_as_xml(
305
+ self,
306
+ sections: list[str],
307
+ config: InjectionConfig,
308
+ ) -> str:
309
+ """Format sections as XML."""
310
+ import html
311
+ items = "\n".join(
312
+ f"<knowledge_item>\n{html.escape(s)}\n</knowledge_item>"
313
+ for s in sections
314
+ )
315
+ return f"<organizational_knowledge>\n{items}\n</organizational_knowledge>"
316
+
317
+ def _format_as_json(
318
+ self,
319
+ sections: list[str],
320
+ config: InjectionConfig,
321
+ ) -> str:
322
+ """Format sections as JSON array."""
323
+ import json
324
+ return json.dumps({"knowledge_items": sections}, indent=2)
325
+
326
+ def build_system_prompt_section(
327
+ self,
328
+ nodes: list[CivilizationNode],
329
+ section_name: str = "ORGANIZATIONAL_CONTEXT",
330
+ ) -> str:
331
+ """
332
+ Build a section suitable for system prompts.
333
+ """
334
+ result = self.inject(nodes)
335
+ if not result.context:
336
+ return ""
337
+
338
+ return f"""
339
+ <{section_name}>
340
+ {result.context}
341
+ </{section_name}>
342
+ """