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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Drive integration for syncing organizational documents.
|
|
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 GDriveConfig:
|
|
19
|
+
"""Configuration for Google Drive sync."""
|
|
20
|
+
credentials_path: str = ""
|
|
21
|
+
folder_ids: list[str] = field(default_factory=list)
|
|
22
|
+
folder_type_mapping: dict[str, NodeType] = field(default_factory=dict)
|
|
23
|
+
sync_interval_minutes: int = 120
|
|
24
|
+
file_types: list[str] = field(default_factory=lambda: [".md", ".txt", ".docx", ".pdf"])
|
|
25
|
+
max_file_size_mb: int = 10
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class GDriveFile:
|
|
30
|
+
"""Represents a Google Drive file."""
|
|
31
|
+
file_id: str
|
|
32
|
+
name: str
|
|
33
|
+
mime_type: str
|
|
34
|
+
folder_id: str
|
|
35
|
+
web_view_link: str
|
|
36
|
+
modified_time: datetime
|
|
37
|
+
size_bytes: int = 0
|
|
38
|
+
content: bytes | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class GDriveSyncResult:
|
|
43
|
+
"""Result of a Google Drive sync operation."""
|
|
44
|
+
success: bool
|
|
45
|
+
files_processed: int = 0
|
|
46
|
+
files_skipped: int = 0
|
|
47
|
+
nodes_created: int = 0
|
|
48
|
+
nodes_updated: int = 0
|
|
49
|
+
errors: list[str] = field(default_factory=list)
|
|
50
|
+
sync_time: datetime = field(default_factory=datetime.utcnow)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GDriveSync:
|
|
54
|
+
"""
|
|
55
|
+
Synchronizes Google Drive documents with Civilization Knowledge Layer.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
SUPPORTED_MIME_TYPES = {
|
|
59
|
+
"application/vnd.google-apps.document": "gdoc",
|
|
60
|
+
"text/plain": "txt",
|
|
61
|
+
"text/markdown": "md",
|
|
62
|
+
"application/pdf": "pdf",
|
|
63
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
org_id: str,
|
|
69
|
+
config: GDriveConfig,
|
|
70
|
+
ingestor: DocumentIngestor | None = None,
|
|
71
|
+
):
|
|
72
|
+
self.org_id = org_id
|
|
73
|
+
self.config = config
|
|
74
|
+
self.ingestor = ingestor
|
|
75
|
+
self._last_sync: datetime | None = None
|
|
76
|
+
self._file_cache: dict[str, datetime] = {} # file_id -> last_modified
|
|
77
|
+
|
|
78
|
+
async def sync_folder(
|
|
79
|
+
self,
|
|
80
|
+
folder_id: str,
|
|
81
|
+
node_type: NodeType | None = None,
|
|
82
|
+
recursive: bool = True,
|
|
83
|
+
) -> GDriveSyncResult:
|
|
84
|
+
"""
|
|
85
|
+
Sync a Google Drive folder to civilization nodes.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
folder_id: Google Drive folder ID
|
|
89
|
+
node_type: Optional node type to assign to all files
|
|
90
|
+
recursive: Whether to sync subfolders
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
GDriveSyncResult with sync statistics
|
|
94
|
+
"""
|
|
95
|
+
result = GDriveSyncResult(success=False)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Fetch files from folder
|
|
99
|
+
files = await self._list_folder_files(folder_id, recursive)
|
|
100
|
+
|
|
101
|
+
for file in files:
|
|
102
|
+
try:
|
|
103
|
+
# Skip if not modified since last sync
|
|
104
|
+
if self._should_skip_file(file):
|
|
105
|
+
result.files_skipped += 1
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Process file
|
|
109
|
+
await self._process_file(file, node_type, result)
|
|
110
|
+
result.files_processed += 1
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
result.errors.append(f"Error processing file {file.name}: {e}")
|
|
114
|
+
|
|
115
|
+
result.success = True
|
|
116
|
+
self._last_sync = datetime.utcnow()
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.exception("Google Drive sync failed")
|
|
120
|
+
result.errors.append(str(e))
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
async def sync_all_folders(self) -> GDriveSyncResult:
|
|
125
|
+
"""Sync all configured folders."""
|
|
126
|
+
total_result = GDriveSyncResult(success=True)
|
|
127
|
+
|
|
128
|
+
for folder_id in self.config.folder_ids:
|
|
129
|
+
node_type = self.config.folder_type_mapping.get(folder_id)
|
|
130
|
+
result = await self.sync_folder(folder_id, node_type)
|
|
131
|
+
|
|
132
|
+
total_result.files_processed += result.files_processed
|
|
133
|
+
total_result.files_skipped += result.files_skipped
|
|
134
|
+
total_result.nodes_created += result.nodes_created
|
|
135
|
+
total_result.nodes_updated += result.nodes_updated
|
|
136
|
+
total_result.errors.extend(result.errors)
|
|
137
|
+
|
|
138
|
+
if not result.success:
|
|
139
|
+
total_result.success = False
|
|
140
|
+
|
|
141
|
+
return total_result
|
|
142
|
+
|
|
143
|
+
async def sync_file(self, file_id: str) -> CivilizationNode | None:
|
|
144
|
+
"""Sync a single file by ID."""
|
|
145
|
+
file = await self._get_file(file_id)
|
|
146
|
+
if not file:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
result = GDriveSyncResult(success=False)
|
|
150
|
+
await self._process_file(file, None, result)
|
|
151
|
+
|
|
152
|
+
return None # Would return created/updated node
|
|
153
|
+
|
|
154
|
+
async def _list_folder_files(
|
|
155
|
+
self,
|
|
156
|
+
folder_id: str,
|
|
157
|
+
recursive: bool,
|
|
158
|
+
) -> list[GDriveFile]:
|
|
159
|
+
"""
|
|
160
|
+
List files in a Google Drive folder.
|
|
161
|
+
This is a stub - actual implementation would use Google Drive API.
|
|
162
|
+
"""
|
|
163
|
+
# In real implementation:
|
|
164
|
+
# from google.oauth2 import service_account
|
|
165
|
+
# from googleapiclient.discovery import build
|
|
166
|
+
#
|
|
167
|
+
# creds = service_account.Credentials.from_service_account_file(
|
|
168
|
+
# self.config.credentials_path
|
|
169
|
+
# )
|
|
170
|
+
# service = build('drive', 'v3', credentials=creds)
|
|
171
|
+
# results = service.files().list(
|
|
172
|
+
# q=f"'{folder_id}' in parents",
|
|
173
|
+
# pageSize=100,
|
|
174
|
+
# fields="files(id, name, mimeType, modifiedTime, size, webViewLink)"
|
|
175
|
+
# ).execute()
|
|
176
|
+
|
|
177
|
+
logger.info(f"Would list files from Google Drive folder: {folder_id}")
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
async def _get_file(self, file_id: str) -> GDriveFile | None:
|
|
181
|
+
"""Fetch a single file with content."""
|
|
182
|
+
logger.info(f"Would fetch Google Drive file: {file_id}")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
async def _download_file_content(self, file: GDriveFile) -> bytes | None:
|
|
186
|
+
"""Download file content."""
|
|
187
|
+
# Check size limit
|
|
188
|
+
max_size = self.config.max_file_size_mb * 1024 * 1024
|
|
189
|
+
if file.size_bytes > max_size:
|
|
190
|
+
logger.warning(f"File {file.name} exceeds size limit")
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
# In real implementation, would download file content
|
|
194
|
+
logger.info(f"Would download content for file: {file.name}")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def _should_skip_file(self, file: GDriveFile) -> bool:
|
|
198
|
+
"""Check if file should be skipped (not modified)."""
|
|
199
|
+
# Check mime type
|
|
200
|
+
if file.mime_type not in self.SUPPORTED_MIME_TYPES:
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
# Check extension
|
|
204
|
+
ext = "." + file.name.split(".")[-1].lower() if "." in file.name else ""
|
|
205
|
+
if ext and ext not in self.config.file_types:
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
# Check if modified since last seen
|
|
209
|
+
last_seen = self._file_cache.get(file.file_id)
|
|
210
|
+
if last_seen and file.modified_time <= last_seen:
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
async def _process_file(
|
|
216
|
+
self,
|
|
217
|
+
file: GDriveFile,
|
|
218
|
+
node_type: NodeType | None,
|
|
219
|
+
result: GDriveSyncResult,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Process a Google Drive file and create/update civilization node."""
|
|
222
|
+
if not self.ingestor:
|
|
223
|
+
logger.warning("No ingestor configured")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Download content
|
|
227
|
+
content = await self._download_file_content(file)
|
|
228
|
+
if not content:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# Ingest
|
|
232
|
+
from hyperclaw.taint import TaintedInput
|
|
233
|
+
try:
|
|
234
|
+
ingest_result = await self.ingestor.ingest(
|
|
235
|
+
content=content,
|
|
236
|
+
filename=file.name,
|
|
237
|
+
source="gdrive",
|
|
238
|
+
source_url=file.web_view_link,
|
|
239
|
+
node_type_hint=node_type,
|
|
240
|
+
)
|
|
241
|
+
except TaintedInput as e: # connector content never enters the knowledge layer
|
|
242
|
+
result.errors.append(e.user_message)
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if ingest_result.success:
|
|
246
|
+
result.nodes_created += 1
|
|
247
|
+
self._file_cache[file.file_id] = file.modified_time
|
|
248
|
+
else:
|
|
249
|
+
result.errors.extend(ingest_result.errors)
|
|
250
|
+
|
|
251
|
+
def get_sync_status(self) -> dict:
|
|
252
|
+
"""Get current sync status."""
|
|
253
|
+
return {
|
|
254
|
+
"last_sync": self._last_sync.isoformat() if self._last_sync else None,
|
|
255
|
+
"configured_folders": self.config.folder_ids,
|
|
256
|
+
"sync_interval_minutes": self.config.sync_interval_minutes,
|
|
257
|
+
"cached_files": len(self._file_cache),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def clear_cache(self) -> None:
|
|
261
|
+
"""Clear the file modification cache (forces full re-sync)."""
|
|
262
|
+
self._file_cache.clear()
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notion 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, uuid4
|
|
10
|
+
|
|
11
|
+
from ..schema import CivilizationNode, NodeType, NodeStatus
|
|
12
|
+
from ..ingestion.document_ingestor import DocumentIngestor
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class NotionConfig:
|
|
19
|
+
"""Configuration for Notion sync."""
|
|
20
|
+
api_key: str = ""
|
|
21
|
+
workspace_id: str | None = None
|
|
22
|
+
database_ids: dict[NodeType, str] = field(default_factory=dict)
|
|
23
|
+
sync_interval_minutes: int = 60
|
|
24
|
+
auto_create_nodes: bool = True
|
|
25
|
+
bidirectional: bool = False # If True, also push changes back to Notion
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class NotionPage:
|
|
30
|
+
"""Represents a Notion page."""
|
|
31
|
+
page_id: str
|
|
32
|
+
title: str
|
|
33
|
+
url: str
|
|
34
|
+
content: str
|
|
35
|
+
properties: dict = field(default_factory=dict)
|
|
36
|
+
last_edited: datetime | None = None
|
|
37
|
+
parent_id: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SyncResult:
|
|
42
|
+
"""Result of a sync operation."""
|
|
43
|
+
success: bool
|
|
44
|
+
pages_synced: int = 0
|
|
45
|
+
nodes_created: int = 0
|
|
46
|
+
nodes_updated: int = 0
|
|
47
|
+
errors: list[str] = field(default_factory=list)
|
|
48
|
+
sync_time: datetime = field(default_factory=datetime.utcnow)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NotionSync:
|
|
52
|
+
"""
|
|
53
|
+
Synchronizes Notion workspace content with Civilization Knowledge Layer.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
org_id: str,
|
|
59
|
+
config: NotionConfig,
|
|
60
|
+
ingestor: DocumentIngestor | None = None,
|
|
61
|
+
):
|
|
62
|
+
self.org_id = org_id
|
|
63
|
+
self.config = config
|
|
64
|
+
self.ingestor = ingestor
|
|
65
|
+
self._last_sync: datetime | None = None
|
|
66
|
+
self._sync_cursor: dict[str, str] = {}
|
|
67
|
+
|
|
68
|
+
async def sync_database(
|
|
69
|
+
self,
|
|
70
|
+
database_id: str,
|
|
71
|
+
node_type: NodeType | None = None,
|
|
72
|
+
) -> SyncResult:
|
|
73
|
+
"""
|
|
74
|
+
Sync a Notion database to civilization nodes.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
database_id: Notion database ID
|
|
78
|
+
node_type: Optional node type to assign
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
SyncResult with sync statistics
|
|
82
|
+
"""
|
|
83
|
+
result = SyncResult(success=False)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Fetch pages from Notion
|
|
87
|
+
pages = await self._fetch_database_pages(database_id)
|
|
88
|
+
result.pages_synced = len(pages)
|
|
89
|
+
|
|
90
|
+
# Process each page
|
|
91
|
+
for page in pages:
|
|
92
|
+
try:
|
|
93
|
+
await self._process_page(page, node_type, result)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
result.errors.append(f"Error processing page {page.page_id}: {e}")
|
|
96
|
+
|
|
97
|
+
result.success = True
|
|
98
|
+
self._last_sync = datetime.utcnow()
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.exception("Notion sync failed")
|
|
102
|
+
result.errors.append(str(e))
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
async def sync_all_databases(self) -> SyncResult:
|
|
107
|
+
"""Sync all configured databases."""
|
|
108
|
+
total_result = SyncResult(success=True)
|
|
109
|
+
|
|
110
|
+
for node_type, database_id in self.config.database_ids.items():
|
|
111
|
+
result = await self.sync_database(database_id, node_type)
|
|
112
|
+
total_result.pages_synced += result.pages_synced
|
|
113
|
+
total_result.nodes_created += result.nodes_created
|
|
114
|
+
total_result.nodes_updated += result.nodes_updated
|
|
115
|
+
total_result.errors.extend(result.errors)
|
|
116
|
+
if not result.success:
|
|
117
|
+
total_result.success = False
|
|
118
|
+
|
|
119
|
+
return total_result
|
|
120
|
+
|
|
121
|
+
async def sync_page(self, page_id: str) -> CivilizationNode | None:
|
|
122
|
+
"""Sync a single Notion page."""
|
|
123
|
+
page = await self._fetch_page(page_id)
|
|
124
|
+
if not page:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
result = SyncResult(success=False)
|
|
128
|
+
await self._process_page(page, None, result)
|
|
129
|
+
|
|
130
|
+
return None # Would return the created/updated node
|
|
131
|
+
|
|
132
|
+
async def _fetch_database_pages(self, database_id: str) -> list[NotionPage]:
|
|
133
|
+
"""
|
|
134
|
+
Fetch all pages from a Notion database.
|
|
135
|
+
This is a stub - actual implementation would use Notion API.
|
|
136
|
+
"""
|
|
137
|
+
# In real implementation:
|
|
138
|
+
# async with httpx.AsyncClient() as client:
|
|
139
|
+
# response = await client.post(
|
|
140
|
+
# f"https://api.notion.com/v1/databases/{database_id}/query",
|
|
141
|
+
# headers={
|
|
142
|
+
# "Authorization": f"Bearer {self.config.api_key}",
|
|
143
|
+
# "Notion-Version": "2022-06-28",
|
|
144
|
+
# },
|
|
145
|
+
# json={"page_size": 100},
|
|
146
|
+
# )
|
|
147
|
+
# ...
|
|
148
|
+
|
|
149
|
+
logger.info(f"Would fetch pages from Notion database: {database_id}")
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
async def _fetch_page(self, page_id: str) -> NotionPage | None:
|
|
153
|
+
"""
|
|
154
|
+
Fetch a single Notion page with content.
|
|
155
|
+
This is a stub - actual implementation would use Notion API.
|
|
156
|
+
"""
|
|
157
|
+
logger.info(f"Would fetch Notion page: {page_id}")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
async def _process_page(
|
|
161
|
+
self,
|
|
162
|
+
page: NotionPage,
|
|
163
|
+
node_type: NodeType | None,
|
|
164
|
+
result: SyncResult,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Process a Notion page and create/update civilization node."""
|
|
167
|
+
if not self.ingestor:
|
|
168
|
+
logger.warning("No ingestor configured, skipping page processing")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Determine node type from page properties or use provided type
|
|
172
|
+
detected_type = node_type or self._detect_node_type(page)
|
|
173
|
+
|
|
174
|
+
# Ingest the page content
|
|
175
|
+
from hyperclaw.taint import TaintedInput
|
|
176
|
+
try:
|
|
177
|
+
ingest_result = await self.ingestor.ingest(
|
|
178
|
+
content=page.content,
|
|
179
|
+
filename=f"{page.title}.md",
|
|
180
|
+
source="notion",
|
|
181
|
+
source_url=page.url,
|
|
182
|
+
node_type_hint=detected_type,
|
|
183
|
+
)
|
|
184
|
+
except TaintedInput as e: # connector content never enters the knowledge layer
|
|
185
|
+
result.errors.append(e.user_message)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if ingest_result.success:
|
|
189
|
+
result.nodes_created += 1
|
|
190
|
+
else:
|
|
191
|
+
result.errors.extend(ingest_result.errors)
|
|
192
|
+
|
|
193
|
+
def _detect_node_type(self, page: NotionPage) -> NodeType:
|
|
194
|
+
"""Detect node type from page properties."""
|
|
195
|
+
# Check for type property
|
|
196
|
+
type_prop = page.properties.get("Type", {}).get("select", {}).get("name", "")
|
|
197
|
+
type_map = {
|
|
198
|
+
"SOP": NodeType.SOP,
|
|
199
|
+
"Procedure": NodeType.SOP,
|
|
200
|
+
"Checklist": NodeType.CHECKLIST,
|
|
201
|
+
"Runbook": NodeType.RUNBOOK,
|
|
202
|
+
"Role": NodeType.ROLE,
|
|
203
|
+
"Job Description": NodeType.JOB_DESCRIPTION,
|
|
204
|
+
"Policy": NodeType.POLICY,
|
|
205
|
+
}
|
|
206
|
+
return type_map.get(type_prop, NodeType.KNOWLEDGE_ARTICLE)
|
|
207
|
+
|
|
208
|
+
async def push_to_notion(
|
|
209
|
+
self,
|
|
210
|
+
node: CivilizationNode,
|
|
211
|
+
database_id: str | None = None,
|
|
212
|
+
) -> str | None:
|
|
213
|
+
"""
|
|
214
|
+
Push a civilization node back to Notion.
|
|
215
|
+
Returns the Notion page ID if successful.
|
|
216
|
+
"""
|
|
217
|
+
if not self.config.bidirectional:
|
|
218
|
+
logger.warning("Bidirectional sync not enabled")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
db_id = database_id or self.config.database_ids.get(node.node_type)
|
|
222
|
+
if not db_id:
|
|
223
|
+
logger.warning(f"No database configured for node type: {node.node_type}")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
# In real implementation, would create/update Notion page
|
|
227
|
+
logger.info(f"Would push node {node.id} to Notion database {db_id}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def get_sync_status(self) -> dict:
|
|
231
|
+
"""Get current sync status."""
|
|
232
|
+
return {
|
|
233
|
+
"last_sync": self._last_sync.isoformat() if self._last_sync else None,
|
|
234
|
+
"configured_databases": list(self.config.database_ids.keys()),
|
|
235
|
+
"sync_interval_minutes": self.config.sync_interval_minutes,
|
|
236
|
+
"bidirectional": self.config.bidirectional,
|
|
237
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Versioning module for Civilization Knowledge Layer.
|
|
3
|
+
Handles version tracking, diffs, and staleness detection.
|
|
4
|
+
"""
|
|
5
|
+
from .version_manager import VersionManager, NodeVersion, VersionHistory
|
|
6
|
+
from .diff_engine import DiffEngine, NodeDiff, DiffType
|
|
7
|
+
from .staleness_detector import StalenessDetector, StalenessLevel, StalenessReport
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"VersionManager",
|
|
11
|
+
"NodeVersion",
|
|
12
|
+
"VersionHistory",
|
|
13
|
+
"DiffEngine",
|
|
14
|
+
"NodeDiff",
|
|
15
|
+
"DiffType",
|
|
16
|
+
"StalenessDetector",
|
|
17
|
+
"StalenessLevel",
|
|
18
|
+
"StalenessReport",
|
|
19
|
+
]
|