hyperagents 1.2.3rc5.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
civilization/store.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CivilizationStore — all DB operations for civilization nodes.
|
|
3
|
+
Wraps asyncpg pool (or stub when pool=None for testing).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from hyperclaw.taint import training_sink
|
|
13
|
+
|
|
14
|
+
from .schema import CivilizationNode, NodeType, NodeStatus
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CivilizationStore:
|
|
20
|
+
def __init__(self, pool=None):
|
|
21
|
+
self.pool = pool # asyncpg pool; None = no-DB mode (tests)
|
|
22
|
+
|
|
23
|
+
@training_sink("civilization.store.save")
|
|
24
|
+
async def save(self, node: CivilizationNode) -> CivilizationNode:
|
|
25
|
+
"""Upsert a node into civilization_nodes."""
|
|
26
|
+
if not self.pool:
|
|
27
|
+
return node
|
|
28
|
+
content = node.model_dump(mode="json", exclude={"embedding"})
|
|
29
|
+
async with self.pool.acquire() as conn:
|
|
30
|
+
await conn.execute("""
|
|
31
|
+
INSERT INTO civilization_nodes
|
|
32
|
+
(id, org_id, node_type, title, status, version, owner_id,
|
|
33
|
+
tags, source, source_url, content, embedding, metadata,
|
|
34
|
+
created_at, updated_at)
|
|
35
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::vector,$13,$14,$15)
|
|
36
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
37
|
+
title=EXCLUDED.title, status=EXCLUDED.status,
|
|
38
|
+
version=EXCLUDED.version, content=EXCLUDED.content,
|
|
39
|
+
embedding=EXCLUDED.embedding, updated_at=EXCLUDED.updated_at
|
|
40
|
+
""",
|
|
41
|
+
node.id, node.org_id, node.node_type.value, node.title,
|
|
42
|
+
node.status.value, node.version, node.owner_id,
|
|
43
|
+
node.tags, node.source, node.source_url,
|
|
44
|
+
json.dumps(content),
|
|
45
|
+
node.embedding,
|
|
46
|
+
json.dumps(node.metadata),
|
|
47
|
+
node.created_at, node.updated_at)
|
|
48
|
+
return node
|
|
49
|
+
|
|
50
|
+
async def get(self, node_id: UUID, org_id: str) -> CivilizationNode | None:
|
|
51
|
+
if not self.pool:
|
|
52
|
+
return None
|
|
53
|
+
async with self.pool.acquire() as conn:
|
|
54
|
+
row = await conn.fetchrow(
|
|
55
|
+
"SELECT * FROM civilization_nodes WHERE id=$1 AND org_id=$2",
|
|
56
|
+
node_id, org_id
|
|
57
|
+
)
|
|
58
|
+
if not row:
|
|
59
|
+
return None
|
|
60
|
+
return self._row_to_node(row)
|
|
61
|
+
|
|
62
|
+
async def list_by_type(self, org_id: str, node_type: NodeType,
|
|
63
|
+
limit: int = 100) -> list[CivilizationNode]:
|
|
64
|
+
if not self.pool:
|
|
65
|
+
return []
|
|
66
|
+
async with self.pool.acquire() as conn:
|
|
67
|
+
rows = await conn.fetch(
|
|
68
|
+
"SELECT * FROM civilization_nodes WHERE org_id=$1 AND node_type=$2 LIMIT $3",
|
|
69
|
+
org_id, node_type.value, limit
|
|
70
|
+
)
|
|
71
|
+
return [self._row_to_node(r) for r in rows]
|
|
72
|
+
|
|
73
|
+
async def list_all(self, org_id: str, limit: int = 500) -> list[CivilizationNode]:
|
|
74
|
+
if not self.pool:
|
|
75
|
+
return []
|
|
76
|
+
async with self.pool.acquire() as conn:
|
|
77
|
+
rows = await conn.fetch(
|
|
78
|
+
"SELECT * FROM civilization_nodes WHERE org_id=$1 LIMIT $2",
|
|
79
|
+
org_id, limit
|
|
80
|
+
)
|
|
81
|
+
return [self._row_to_node(r) for r in rows]
|
|
82
|
+
|
|
83
|
+
async def search_by_embedding(self, org_id: str, embedding: list[float],
|
|
84
|
+
top_k: int = 10,
|
|
85
|
+
node_types: list[NodeType] | None = None) -> list[dict]:
|
|
86
|
+
"""Vector similarity search."""
|
|
87
|
+
if not self.pool:
|
|
88
|
+
return []
|
|
89
|
+
type_filter = ""
|
|
90
|
+
args = [org_id, embedding, top_k]
|
|
91
|
+
if node_types:
|
|
92
|
+
placeholders = ",".join(f"${i+4}" for i in range(len(node_types)))
|
|
93
|
+
type_filter = f"AND node_type IN ({placeholders})"
|
|
94
|
+
args.extend([t.value for t in node_types])
|
|
95
|
+
async with self.pool.acquire() as conn:
|
|
96
|
+
rows = await conn.fetch(f"""
|
|
97
|
+
SELECT *, 1 - (embedding <=> $2::vector) AS score
|
|
98
|
+
FROM civilization_nodes
|
|
99
|
+
WHERE org_id=$1 {type_filter}
|
|
100
|
+
ORDER BY embedding <=> $2::vector
|
|
101
|
+
LIMIT $3
|
|
102
|
+
""", *args)
|
|
103
|
+
return [dict(r) for r in rows]
|
|
104
|
+
|
|
105
|
+
async def search_by_tags(self, org_id: str, tags: list[str],
|
|
106
|
+
limit: int = 50) -> list[CivilizationNode]:
|
|
107
|
+
"""Find nodes that have any of the specified tags."""
|
|
108
|
+
if not self.pool:
|
|
109
|
+
return []
|
|
110
|
+
async with self.pool.acquire() as conn:
|
|
111
|
+
rows = await conn.fetch(
|
|
112
|
+
"SELECT * FROM civilization_nodes WHERE org_id=$1 AND tags && $2 LIMIT $3",
|
|
113
|
+
org_id, tags, limit
|
|
114
|
+
)
|
|
115
|
+
return [self._row_to_node(r) for r in rows]
|
|
116
|
+
|
|
117
|
+
async def search_by_owner(self, org_id: str, owner_id: str,
|
|
118
|
+
limit: int = 100) -> list[CivilizationNode]:
|
|
119
|
+
"""Find all nodes owned by a specific person."""
|
|
120
|
+
if not self.pool:
|
|
121
|
+
return []
|
|
122
|
+
async with self.pool.acquire() as conn:
|
|
123
|
+
rows = await conn.fetch(
|
|
124
|
+
"SELECT * FROM civilization_nodes WHERE org_id=$1 AND owner_id=$2 LIMIT $3",
|
|
125
|
+
org_id, owner_id, limit
|
|
126
|
+
)
|
|
127
|
+
return [self._row_to_node(r) for r in rows]
|
|
128
|
+
|
|
129
|
+
async def count_by_type(self, org_id: str) -> dict[str, int]:
|
|
130
|
+
if not self.pool:
|
|
131
|
+
return {}
|
|
132
|
+
async with self.pool.acquire() as conn:
|
|
133
|
+
rows = await conn.fetch(
|
|
134
|
+
"SELECT node_type, COUNT(*) as cnt FROM civilization_nodes WHERE org_id=$1 GROUP BY node_type",
|
|
135
|
+
org_id
|
|
136
|
+
)
|
|
137
|
+
return {r["node_type"]: r["cnt"] for r in rows}
|
|
138
|
+
|
|
139
|
+
async def delete(self, node_id: UUID, org_id: str) -> bool:
|
|
140
|
+
if not self.pool:
|
|
141
|
+
return True
|
|
142
|
+
async with self.pool.acquire() as conn:
|
|
143
|
+
result = await conn.execute(
|
|
144
|
+
"DELETE FROM civilization_nodes WHERE id=$1 AND org_id=$2",
|
|
145
|
+
node_id, org_id
|
|
146
|
+
)
|
|
147
|
+
return result == "DELETE 1"
|
|
148
|
+
|
|
149
|
+
@training_sink("civilization.store.update_status")
|
|
150
|
+
async def update_status(self, node_id: UUID, org_id: str,
|
|
151
|
+
new_status: NodeStatus) -> bool:
|
|
152
|
+
"""Update a node's status."""
|
|
153
|
+
if not self.pool:
|
|
154
|
+
return True
|
|
155
|
+
async with self.pool.acquire() as conn:
|
|
156
|
+
result = await conn.execute(
|
|
157
|
+
"UPDATE civilization_nodes SET status=$3, updated_at=$4 WHERE id=$1 AND org_id=$2",
|
|
158
|
+
node_id, org_id, new_status.value, datetime.utcnow()
|
|
159
|
+
)
|
|
160
|
+
return "UPDATE 1" in result
|
|
161
|
+
|
|
162
|
+
@training_sink("civilization.store.update_embedding")
|
|
163
|
+
async def update_embedding(self, node_id: UUID, org_id: str,
|
|
164
|
+
embedding: list[float]) -> bool:
|
|
165
|
+
"""Update a node's embedding vector."""
|
|
166
|
+
if not self.pool:
|
|
167
|
+
return True
|
|
168
|
+
async with self.pool.acquire() as conn:
|
|
169
|
+
result = await conn.execute(
|
|
170
|
+
"UPDATE civilization_nodes SET embedding=$3::vector, updated_at=$4 WHERE id=$1 AND org_id=$2",
|
|
171
|
+
node_id, org_id, embedding, datetime.utcnow()
|
|
172
|
+
)
|
|
173
|
+
return "UPDATE 1" in result
|
|
174
|
+
|
|
175
|
+
def _row_to_node(self, row) -> CivilizationNode:
|
|
176
|
+
content = json.loads(row["content"]) if isinstance(row["content"], str) else row["content"]
|
|
177
|
+
return CivilizationNode(**{**content, "id": row["id"], "org_id": row["org_id"]})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync module for Civilization Knowledge Layer.
|
|
3
|
+
Handles synchronization with external knowledge management systems.
|
|
4
|
+
"""
|
|
5
|
+
from .notion_sync import NotionSync, NotionConfig
|
|
6
|
+
from .gdrive_sync import GDriveSync, GDriveConfig
|
|
7
|
+
from .confluence_sync import ConfluenceSync, ConfluenceConfig
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"NotionSync",
|
|
11
|
+
"NotionConfig",
|
|
12
|
+
"GDriveSync",
|
|
13
|
+
"GDriveConfig",
|
|
14
|
+
"ConfluenceSync",
|
|
15
|
+
"ConfluenceConfig",
|
|
16
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Confluence integration for syncing organizational knowledge.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from ..schema import CivilizationNode, NodeType
|
|
12
|
+
from ..ingestion.document_ingestor import DocumentIngestor
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConfluenceConfig:
|
|
19
|
+
"""Configuration for Confluence sync."""
|
|
20
|
+
base_url: str = "" # e.g., "https://your-domain.atlassian.net/wiki"
|
|
21
|
+
username: str = ""
|
|
22
|
+
api_token: str = ""
|
|
23
|
+
space_keys: list[str] = field(default_factory=list)
|
|
24
|
+
space_type_mapping: dict[str, NodeType] = field(default_factory=dict)
|
|
25
|
+
sync_interval_minutes: int = 120
|
|
26
|
+
include_attachments: bool = False
|
|
27
|
+
label_filters: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ConfluencePage:
|
|
32
|
+
"""Represents a Confluence page."""
|
|
33
|
+
page_id: str
|
|
34
|
+
title: str
|
|
35
|
+
space_key: str
|
|
36
|
+
web_url: str
|
|
37
|
+
body_html: str
|
|
38
|
+
body_storage: str = "" # Confluence storage format
|
|
39
|
+
labels: list[str] = field(default_factory=list)
|
|
40
|
+
version: int = 1
|
|
41
|
+
last_modified: datetime | None = None
|
|
42
|
+
parent_id: str | None = None
|
|
43
|
+
ancestors: list[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ConfluenceSyncResult:
|
|
48
|
+
"""Result of a Confluence sync operation."""
|
|
49
|
+
success: bool
|
|
50
|
+
pages_synced: int = 0
|
|
51
|
+
pages_skipped: int = 0
|
|
52
|
+
nodes_created: int = 0
|
|
53
|
+
nodes_updated: int = 0
|
|
54
|
+
errors: list[str] = field(default_factory=list)
|
|
55
|
+
sync_time: datetime = field(default_factory=datetime.utcnow)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ConfluenceSync:
|
|
59
|
+
"""
|
|
60
|
+
Synchronizes Confluence spaces/pages with Civilization Knowledge Layer.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
LABEL_TYPE_MAPPING = {
|
|
64
|
+
"sop": NodeType.SOP,
|
|
65
|
+
"procedure": NodeType.SOP,
|
|
66
|
+
"checklist": NodeType.CHECKLIST,
|
|
67
|
+
"runbook": NodeType.RUNBOOK,
|
|
68
|
+
"playbook": NodeType.RUNBOOK,
|
|
69
|
+
"role": NodeType.ROLE,
|
|
70
|
+
"job-description": NodeType.JOB_DESCRIPTION,
|
|
71
|
+
"policy": NodeType.POLICY,
|
|
72
|
+
"workflow": NodeType.WORKFLOW,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
org_id: str,
|
|
78
|
+
config: ConfluenceConfig,
|
|
79
|
+
ingestor: DocumentIngestor | None = None,
|
|
80
|
+
):
|
|
81
|
+
self.org_id = org_id
|
|
82
|
+
self.config = config
|
|
83
|
+
self.ingestor = ingestor
|
|
84
|
+
self._last_sync: datetime | None = None
|
|
85
|
+
self._page_cache: dict[str, int] = {} # page_id -> version
|
|
86
|
+
|
|
87
|
+
async def sync_space(
|
|
88
|
+
self,
|
|
89
|
+
space_key: str,
|
|
90
|
+
node_type: NodeType | None = None,
|
|
91
|
+
) -> ConfluenceSyncResult:
|
|
92
|
+
"""
|
|
93
|
+
Sync a Confluence space to civilization nodes.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
space_key: Confluence space key
|
|
97
|
+
node_type: Optional default node type
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ConfluenceSyncResult with sync statistics
|
|
101
|
+
"""
|
|
102
|
+
result = ConfluenceSyncResult(success=False)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Fetch pages from space
|
|
106
|
+
pages = await self._list_space_pages(space_key)
|
|
107
|
+
|
|
108
|
+
for page in pages:
|
|
109
|
+
try:
|
|
110
|
+
# Skip if not modified
|
|
111
|
+
if self._should_skip_page(page):
|
|
112
|
+
result.pages_skipped += 1
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Apply label filters
|
|
116
|
+
if self.config.label_filters:
|
|
117
|
+
if not any(l in page.labels for l in self.config.label_filters):
|
|
118
|
+
result.pages_skipped += 1
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Process page
|
|
122
|
+
await self._process_page(page, node_type, result)
|
|
123
|
+
result.pages_synced += 1
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
result.errors.append(f"Error processing page {page.title}: {e}")
|
|
127
|
+
|
|
128
|
+
result.success = True
|
|
129
|
+
self._last_sync = datetime.utcnow()
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.exception("Confluence sync failed")
|
|
133
|
+
result.errors.append(str(e))
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
async def sync_all_spaces(self) -> ConfluenceSyncResult:
|
|
138
|
+
"""Sync all configured spaces."""
|
|
139
|
+
total_result = ConfluenceSyncResult(success=True)
|
|
140
|
+
|
|
141
|
+
for space_key in self.config.space_keys:
|
|
142
|
+
node_type = self.config.space_type_mapping.get(space_key)
|
|
143
|
+
result = await self.sync_space(space_key, node_type)
|
|
144
|
+
|
|
145
|
+
total_result.pages_synced += result.pages_synced
|
|
146
|
+
total_result.pages_skipped += result.pages_skipped
|
|
147
|
+
total_result.nodes_created += result.nodes_created
|
|
148
|
+
total_result.nodes_updated += result.nodes_updated
|
|
149
|
+
total_result.errors.extend(result.errors)
|
|
150
|
+
|
|
151
|
+
if not result.success:
|
|
152
|
+
total_result.success = False
|
|
153
|
+
|
|
154
|
+
return total_result
|
|
155
|
+
|
|
156
|
+
async def sync_page(self, page_id: str) -> CivilizationNode | None:
|
|
157
|
+
"""Sync a single page by ID."""
|
|
158
|
+
page = await self._get_page(page_id)
|
|
159
|
+
if not page:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
result = ConfluenceSyncResult(success=False)
|
|
163
|
+
await self._process_page(page, None, result)
|
|
164
|
+
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
async def sync_by_label(
|
|
168
|
+
self,
|
|
169
|
+
label: str,
|
|
170
|
+
node_type: NodeType | None = None,
|
|
171
|
+
) -> ConfluenceSyncResult:
|
|
172
|
+
"""Sync all pages with a specific label."""
|
|
173
|
+
result = ConfluenceSyncResult(success=False)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
pages = await self._search_by_label(label)
|
|
177
|
+
|
|
178
|
+
for page in pages:
|
|
179
|
+
try:
|
|
180
|
+
if self._should_skip_page(page):
|
|
181
|
+
result.pages_skipped += 1
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
await self._process_page(page, node_type, result)
|
|
185
|
+
result.pages_synced += 1
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
result.errors.append(f"Error processing page {page.title}: {e}")
|
|
189
|
+
|
|
190
|
+
result.success = True
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.exception("Confluence label sync failed")
|
|
194
|
+
result.errors.append(str(e))
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
async def _list_space_pages(self, space_key: str) -> list[ConfluencePage]:
|
|
199
|
+
"""
|
|
200
|
+
List all pages in a Confluence space.
|
|
201
|
+
This is a stub - actual implementation would use Confluence REST API.
|
|
202
|
+
"""
|
|
203
|
+
# In real implementation:
|
|
204
|
+
# import httpx
|
|
205
|
+
# import base64
|
|
206
|
+
#
|
|
207
|
+
# auth = base64.b64encode(
|
|
208
|
+
# f"{self.config.username}:{self.config.api_token}".encode()
|
|
209
|
+
# ).decode()
|
|
210
|
+
#
|
|
211
|
+
# async with httpx.AsyncClient() as client:
|
|
212
|
+
# response = await client.get(
|
|
213
|
+
# f"{self.config.base_url}/rest/api/content",
|
|
214
|
+
# params={"spaceKey": space_key, "expand": "body.storage,version"},
|
|
215
|
+
# headers={"Authorization": f"Basic {auth}"},
|
|
216
|
+
# )
|
|
217
|
+
|
|
218
|
+
logger.info(f"Would list pages from Confluence space: {space_key}")
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
async def _get_page(self, page_id: str) -> ConfluencePage | None:
|
|
222
|
+
"""Fetch a single Confluence page with content."""
|
|
223
|
+
logger.info(f"Would fetch Confluence page: {page_id}")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
async def _search_by_label(self, label: str) -> list[ConfluencePage]:
|
|
227
|
+
"""Search for pages with a specific label."""
|
|
228
|
+
logger.info(f"Would search Confluence for label: {label}")
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
def _should_skip_page(self, page: ConfluencePage) -> bool:
|
|
232
|
+
"""Check if page should be skipped (not modified)."""
|
|
233
|
+
cached_version = self._page_cache.get(page.page_id)
|
|
234
|
+
if cached_version and page.version <= cached_version:
|
|
235
|
+
return True
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
async def _process_page(
|
|
239
|
+
self,
|
|
240
|
+
page: ConfluencePage,
|
|
241
|
+
node_type: NodeType | None,
|
|
242
|
+
result: ConfluenceSyncResult,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Process a Confluence page and create/update civilization node."""
|
|
245
|
+
if not self.ingestor:
|
|
246
|
+
logger.warning("No ingestor configured")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Detect node type from labels if not specified
|
|
250
|
+
detected_type = node_type or self._detect_node_type(page)
|
|
251
|
+
|
|
252
|
+
# Convert HTML to markdown-ish text
|
|
253
|
+
content = self._convert_confluence_content(page)
|
|
254
|
+
|
|
255
|
+
# Ingest
|
|
256
|
+
from hyperclaw.taint import TaintedInput
|
|
257
|
+
try:
|
|
258
|
+
ingest_result = await self.ingestor.ingest(
|
|
259
|
+
content=content,
|
|
260
|
+
filename=f"{page.title}.md",
|
|
261
|
+
source="confluence",
|
|
262
|
+
source_url=page.web_url,
|
|
263
|
+
node_type_hint=detected_type,
|
|
264
|
+
tags=page.labels,
|
|
265
|
+
)
|
|
266
|
+
except TaintedInput as e: # connector content never enters the knowledge layer
|
|
267
|
+
result.errors.append(e.user_message)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if ingest_result.success:
|
|
271
|
+
result.nodes_created += 1
|
|
272
|
+
self._page_cache[page.page_id] = page.version
|
|
273
|
+
else:
|
|
274
|
+
result.errors.extend(ingest_result.errors)
|
|
275
|
+
|
|
276
|
+
def _detect_node_type(self, page: ConfluencePage) -> NodeType:
|
|
277
|
+
"""Detect node type from page labels."""
|
|
278
|
+
for label in page.labels:
|
|
279
|
+
label_lower = label.lower()
|
|
280
|
+
if label_lower in self.LABEL_TYPE_MAPPING:
|
|
281
|
+
return self.LABEL_TYPE_MAPPING[label_lower]
|
|
282
|
+
return NodeType.KNOWLEDGE_ARTICLE
|
|
283
|
+
|
|
284
|
+
def _convert_confluence_content(self, page: ConfluencePage) -> str:
|
|
285
|
+
"""Convert Confluence storage format to plain text/markdown."""
|
|
286
|
+
import re
|
|
287
|
+
|
|
288
|
+
content = page.body_storage or page.body_html
|
|
289
|
+
|
|
290
|
+
# Simple HTML tag stripping (in production, use a proper HTML parser)
|
|
291
|
+
content = re.sub(r"<br\s*/?>", "\n", content)
|
|
292
|
+
content = re.sub(r"<p[^>]*>", "\n", content)
|
|
293
|
+
content = re.sub(r"</p>", "\n", content)
|
|
294
|
+
content = re.sub(r"<li[^>]*>", "- ", content)
|
|
295
|
+
content = re.sub(r"<h(\d)[^>]*>", r"\n#" + r"\1 ", content)
|
|
296
|
+
content = re.sub(r"</h\d>", "\n", content)
|
|
297
|
+
content = re.sub(r"<[^>]+>", "", content)
|
|
298
|
+
|
|
299
|
+
# Clean up whitespace
|
|
300
|
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
301
|
+
|
|
302
|
+
# Add title
|
|
303
|
+
return f"# {page.title}\n\n{content.strip()}"
|
|
304
|
+
|
|
305
|
+
def get_sync_status(self) -> dict:
|
|
306
|
+
"""Get current sync status."""
|
|
307
|
+
return {
|
|
308
|
+
"last_sync": self._last_sync.isoformat() if self._last_sync else None,
|
|
309
|
+
"configured_spaces": self.config.space_keys,
|
|
310
|
+
"sync_interval_minutes": self.config.sync_interval_minutes,
|
|
311
|
+
"cached_pages": len(self._page_cache),
|
|
312
|
+
"label_filters": self.config.label_filters,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
def clear_cache(self) -> None:
|
|
316
|
+
"""Clear page version cache (forces full re-sync)."""
|
|
317
|
+
self._page_cache.clear()
|