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,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entity and metadata extraction from documents for Civilization Knowledge Layer.
|
|
3
|
+
Uses LLM-based extraction for structured information.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import re
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ExtractedEntity:
|
|
17
|
+
entity_type: str # "person", "role", "tool", "system", "process", "metric"
|
|
18
|
+
name: str
|
|
19
|
+
context: str
|
|
20
|
+
confidence: float = 1.0
|
|
21
|
+
metadata: dict = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ExtractedMetadata:
|
|
26
|
+
title: str | None = None
|
|
27
|
+
purpose: str | None = None
|
|
28
|
+
owner: str | None = None
|
|
29
|
+
department: str | None = None
|
|
30
|
+
tags: list[str] = field(default_factory=list)
|
|
31
|
+
roles_mentioned: list[str] = field(default_factory=list)
|
|
32
|
+
tools_mentioned: list[str] = field(default_factory=list)
|
|
33
|
+
systems_mentioned: list[str] = field(default_factory=list)
|
|
34
|
+
metrics_mentioned: list[str] = field(default_factory=list)
|
|
35
|
+
estimated_complexity: str | None = None # "low", "medium", "high"
|
|
36
|
+
document_type: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MetadataExtractor:
|
|
40
|
+
"""
|
|
41
|
+
Extracts metadata from document text using pattern matching and heuristics.
|
|
42
|
+
For LLM-based extraction, use with an agent wrapper.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Common role patterns
|
|
46
|
+
ROLE_PATTERNS = [
|
|
47
|
+
r"(?:responsible|owner|lead|manager|coordinator|assigned to|handled by)\s*[:\-]?\s*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)",
|
|
48
|
+
r"([A-Z][a-z]+\s+(?:Manager|Director|Lead|Coordinator|Specialist|Engineer|Analyst|Administrator))",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Common tool patterns
|
|
52
|
+
TOOL_PATTERNS = [
|
|
53
|
+
r"(?:using|via|through|in|with)\s+([A-Z][a-zA-Z0-9]+(?:\s+[A-Z][a-zA-Z0-9]+)?)",
|
|
54
|
+
r"(Slack|Jira|Confluence|Notion|Asana|Trello|GitHub|GitLab|Salesforce|HubSpot|Zendesk)",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# System patterns
|
|
58
|
+
SYSTEM_PATTERNS = [
|
|
59
|
+
r"([A-Z][a-zA-Z0-9]+(?:DB|API|Service|System|Platform|Server))",
|
|
60
|
+
r"(?:database|system|service|platform)\s*[:\-]?\s*([A-Z][a-zA-Z0-9]+)",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def extract(self, text: str) -> ExtractedMetadata:
|
|
64
|
+
"""Extract metadata from raw document text."""
|
|
65
|
+
metadata = ExtractedMetadata()
|
|
66
|
+
|
|
67
|
+
# Extract title (first line or heading)
|
|
68
|
+
lines = text.strip().split("\n")
|
|
69
|
+
if lines:
|
|
70
|
+
first_line = lines[0].strip()
|
|
71
|
+
# Remove markdown heading markers
|
|
72
|
+
if first_line.startswith("#"):
|
|
73
|
+
first_line = re.sub(r"^#+\s*", "", first_line)
|
|
74
|
+
if len(first_line) < 200:
|
|
75
|
+
metadata.title = first_line
|
|
76
|
+
|
|
77
|
+
# Extract purpose (look for common patterns)
|
|
78
|
+
purpose_match = re.search(
|
|
79
|
+
r"(?:purpose|objective|goal|overview)\s*[:\-]?\s*(.+?)(?:\n|$)",
|
|
80
|
+
text, re.IGNORECASE
|
|
81
|
+
)
|
|
82
|
+
if purpose_match:
|
|
83
|
+
metadata.purpose = purpose_match.group(1).strip()[:500]
|
|
84
|
+
|
|
85
|
+
# Extract roles
|
|
86
|
+
for pattern in self.ROLE_PATTERNS:
|
|
87
|
+
matches = re.findall(pattern, text)
|
|
88
|
+
metadata.roles_mentioned.extend(matches)
|
|
89
|
+
metadata.roles_mentioned = list(set(metadata.roles_mentioned))
|
|
90
|
+
|
|
91
|
+
# Extract tools
|
|
92
|
+
for pattern in self.TOOL_PATTERNS:
|
|
93
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
94
|
+
metadata.tools_mentioned.extend([m for m in matches if len(m) > 2])
|
|
95
|
+
metadata.tools_mentioned = list(set(metadata.tools_mentioned))
|
|
96
|
+
|
|
97
|
+
# Extract systems
|
|
98
|
+
for pattern in self.SYSTEM_PATTERNS:
|
|
99
|
+
matches = re.findall(pattern, text)
|
|
100
|
+
metadata.systems_mentioned.extend(matches)
|
|
101
|
+
metadata.systems_mentioned = list(set(metadata.systems_mentioned))
|
|
102
|
+
|
|
103
|
+
# Extract tags from hashtags
|
|
104
|
+
hashtags = re.findall(r"#([a-zA-Z][a-zA-Z0-9_-]+)", text)
|
|
105
|
+
metadata.tags = list(set(hashtags))
|
|
106
|
+
|
|
107
|
+
# Estimate complexity based on length and structure
|
|
108
|
+
word_count = len(text.split())
|
|
109
|
+
step_count = len(re.findall(r"(?:step|item|\d+\.)\s", text, re.IGNORECASE))
|
|
110
|
+
if word_count < 300 and step_count < 5:
|
|
111
|
+
metadata.estimated_complexity = "low"
|
|
112
|
+
elif word_count < 1000 and step_count < 15:
|
|
113
|
+
metadata.estimated_complexity = "medium"
|
|
114
|
+
else:
|
|
115
|
+
metadata.estimated_complexity = "high"
|
|
116
|
+
|
|
117
|
+
# Detect document type
|
|
118
|
+
metadata.document_type = self._detect_document_type(text)
|
|
119
|
+
|
|
120
|
+
return metadata
|
|
121
|
+
|
|
122
|
+
def _detect_document_type(self, text: str) -> str | None:
|
|
123
|
+
"""Detect the type of document based on content patterns."""
|
|
124
|
+
text_lower = text.lower()
|
|
125
|
+
|
|
126
|
+
if any(kw in text_lower for kw in ["sop", "standard operating procedure", "procedure:"]):
|
|
127
|
+
return "sop"
|
|
128
|
+
if any(kw in text_lower for kw in ["checklist", "□", "☐", "[ ]", "[x]"]):
|
|
129
|
+
return "checklist"
|
|
130
|
+
if any(kw in text_lower for kw in ["runbook", "playbook", "incident", "on-call"]):
|
|
131
|
+
return "runbook"
|
|
132
|
+
if any(kw in text_lower for kw in ["job description", "responsibilities:", "qualifications:"]):
|
|
133
|
+
return "job_description"
|
|
134
|
+
if any(kw in text_lower for kw in ["org chart", "organization chart", "reporting structure"]):
|
|
135
|
+
return "org_chart"
|
|
136
|
+
if any(kw in text_lower for kw in ["workflow", "process flow", "→", "->"]):
|
|
137
|
+
return "workflow"
|
|
138
|
+
if any(kw in text_lower for kw in ["policy", "must comply", "prohibited", "required"]):
|
|
139
|
+
return "policy"
|
|
140
|
+
return "knowledge_article"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class EntityExtractor:
|
|
144
|
+
"""
|
|
145
|
+
Extracts named entities and their relationships from text.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self):
|
|
149
|
+
self.entity_patterns = {
|
|
150
|
+
"person": [
|
|
151
|
+
r"([A-Z][a-z]+\s+[A-Z][a-z]+)", # First Last
|
|
152
|
+
r"@([a-zA-Z][a-zA-Z0-9_]+)", # @username
|
|
153
|
+
],
|
|
154
|
+
"email": [
|
|
155
|
+
r"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})",
|
|
156
|
+
],
|
|
157
|
+
"url": [
|
|
158
|
+
r"(https?://[^\s<>\"]+)",
|
|
159
|
+
],
|
|
160
|
+
"metric": [
|
|
161
|
+
r"(\d+(?:\.\d+)?%)", # Percentages
|
|
162
|
+
r"(\$\d+(?:,\d{3})*(?:\.\d{2})?)", # Currency
|
|
163
|
+
r"(\d+(?:\.\d+)?\s*(?:hours?|mins?|minutes?|days?|weeks?))", # Time durations
|
|
164
|
+
],
|
|
165
|
+
"date": [
|
|
166
|
+
r"(\d{4}-\d{2}-\d{2})", # ISO date
|
|
167
|
+
r"(\d{1,2}/\d{1,2}/\d{2,4})", # US date
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def extract(self, text: str) -> list[ExtractedEntity]:
|
|
172
|
+
"""Extract all entities from text."""
|
|
173
|
+
entities = []
|
|
174
|
+
|
|
175
|
+
for entity_type, patterns in self.entity_patterns.items():
|
|
176
|
+
for pattern in patterns:
|
|
177
|
+
for match in re.finditer(pattern, text):
|
|
178
|
+
# Get surrounding context (50 chars before and after)
|
|
179
|
+
start = max(0, match.start() - 50)
|
|
180
|
+
end = min(len(text), match.end() + 50)
|
|
181
|
+
context = text[start:end]
|
|
182
|
+
|
|
183
|
+
entities.append(ExtractedEntity(
|
|
184
|
+
entity_type=entity_type,
|
|
185
|
+
name=match.group(1) if match.lastindex else match.group(0),
|
|
186
|
+
context=context,
|
|
187
|
+
confidence=0.8,
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
# Deduplicate by name within each type
|
|
191
|
+
seen = set()
|
|
192
|
+
unique_entities = []
|
|
193
|
+
for entity in entities:
|
|
194
|
+
key = (entity.entity_type, entity.name)
|
|
195
|
+
if key not in seen:
|
|
196
|
+
seen.add(key)
|
|
197
|
+
unique_entities.append(entity)
|
|
198
|
+
|
|
199
|
+
return unique_entities
|
|
200
|
+
|
|
201
|
+
def extract_relationships(self, text: str) -> list[dict]:
|
|
202
|
+
"""
|
|
203
|
+
Extract relationships between entities.
|
|
204
|
+
Returns list of {"source": str, "target": str, "relationship": str}
|
|
205
|
+
"""
|
|
206
|
+
relationships = []
|
|
207
|
+
|
|
208
|
+
# Pattern: "X reports to Y" or "X manages Y"
|
|
209
|
+
report_patterns = [
|
|
210
|
+
(r"([A-Z][a-z]+\s+[A-Z][a-z]+)\s+reports?\s+to\s+([A-Z][a-z]+\s+[A-Z][a-z]+)", "reports_to"),
|
|
211
|
+
(r"([A-Z][a-z]+\s+[A-Z][a-z]+)\s+manages?\s+([A-Z][a-z]+\s+[A-Z][a-z]+)", "manages"),
|
|
212
|
+
(r"([A-Z][a-z]+\s+[A-Z][a-z]+)\s+owns?\s+([A-Z][a-z]+)", "owns"),
|
|
213
|
+
(r"([A-Z][a-z]+\s+[A-Z][a-z]+)\s+is\s+responsible\s+for\s+([A-Z][a-z]+)", "responsible_for"),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
for pattern, rel_type in report_patterns:
|
|
217
|
+
for match in re.finditer(pattern, text, re.IGNORECASE):
|
|
218
|
+
relationships.append({
|
|
219
|
+
"source": match.group(1),
|
|
220
|
+
"target": match.group(2),
|
|
221
|
+
"relationship": rel_type,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return relationships
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interview module for Civilization Knowledge Layer.
|
|
3
|
+
Handles knowledge elicitation through interactive interviews.
|
|
4
|
+
"""
|
|
5
|
+
from .interviewer import Interviewer, InterviewSession, InterviewState
|
|
6
|
+
from .gap_detector import GapDetector, KnowledgeGap, GapSeverity
|
|
7
|
+
from .questioner import Questioner, Question, QuestionType
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Interviewer",
|
|
11
|
+
"InterviewSession",
|
|
12
|
+
"InterviewState",
|
|
13
|
+
"GapDetector",
|
|
14
|
+
"KnowledgeGap",
|
|
15
|
+
"GapSeverity",
|
|
16
|
+
"Questioner",
|
|
17
|
+
"Question",
|
|
18
|
+
"QuestionType",
|
|
19
|
+
]
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gap detector for identifying missing or incomplete knowledge in the Civilization layer.
|
|
3
|
+
Analyzes existing nodes to find coverage gaps, outdated content, and missing relationships.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from ..schema import (
|
|
14
|
+
NodeType, NodeStatus, CivilizationNode,
|
|
15
|
+
SOP, Role, Person, Checklist, Runbook, Workflow, OrgChart,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GapSeverity(str, Enum):
|
|
22
|
+
LOW = "low"
|
|
23
|
+
MEDIUM = "medium"
|
|
24
|
+
HIGH = "high"
|
|
25
|
+
CRITICAL = "critical"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GapType(str, Enum):
|
|
29
|
+
MISSING_NODE = "missing_node"
|
|
30
|
+
INCOMPLETE_NODE = "incomplete_node"
|
|
31
|
+
ORPHAN_NODE = "orphan_node"
|
|
32
|
+
MISSING_RELATIONSHIP = "missing_relationship"
|
|
33
|
+
STALE_CONTENT = "stale_content"
|
|
34
|
+
MISSING_OWNER = "missing_owner"
|
|
35
|
+
COVERAGE_GAP = "coverage_gap"
|
|
36
|
+
INCONSISTENCY = "inconsistency"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class KnowledgeGap:
|
|
41
|
+
"""Represents a detected gap in organizational knowledge."""
|
|
42
|
+
id: UUID
|
|
43
|
+
gap_type: GapType
|
|
44
|
+
severity: GapSeverity
|
|
45
|
+
title: str
|
|
46
|
+
description: str
|
|
47
|
+
affected_node_id: UUID | None = None
|
|
48
|
+
affected_node_type: NodeType | None = None
|
|
49
|
+
suggested_action: str | None = None
|
|
50
|
+
detected_at: datetime = field(default_factory=datetime.utcnow)
|
|
51
|
+
resolved_at: datetime | None = None
|
|
52
|
+
metadata: dict = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_resolved(self) -> bool:
|
|
56
|
+
return self.resolved_at is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class GapReport:
|
|
61
|
+
"""Summary report of knowledge gaps."""
|
|
62
|
+
org_id: str
|
|
63
|
+
generated_at: datetime = field(default_factory=datetime.utcnow)
|
|
64
|
+
gaps: list[KnowledgeGap] = field(default_factory=list)
|
|
65
|
+
coverage_score: float = 0.0 # 0-100
|
|
66
|
+
health_score: float = 0.0 # 0-100
|
|
67
|
+
recommendations: list[str] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def critical_count(self) -> int:
|
|
71
|
+
return sum(1 for g in self.gaps if g.severity == GapSeverity.CRITICAL)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def high_count(self) -> int:
|
|
75
|
+
return sum(1 for g in self.gaps if g.severity == GapSeverity.HIGH)
|
|
76
|
+
|
|
77
|
+
def by_type(self, gap_type: GapType) -> list[KnowledgeGap]:
|
|
78
|
+
return [g for g in self.gaps if g.gap_type == gap_type]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GapDetector:
|
|
82
|
+
"""
|
|
83
|
+
Detects gaps and inconsistencies in organizational knowledge.
|
|
84
|
+
Analyzes nodes, relationships, and coverage to identify areas needing attention.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
# Thresholds for gap detection
|
|
88
|
+
STALE_DAYS_WARNING = 90
|
|
89
|
+
STALE_DAYS_CRITICAL = 180
|
|
90
|
+
MIN_SOP_STEPS = 2
|
|
91
|
+
MIN_CHECKLIST_ITEMS = 2
|
|
92
|
+
MIN_ROLE_RESPONSIBILITIES = 3
|
|
93
|
+
|
|
94
|
+
def __init__(self, org_id: str, store=None):
|
|
95
|
+
self.org_id = org_id
|
|
96
|
+
self.store = store
|
|
97
|
+
|
|
98
|
+
async def analyze(self, nodes: list[CivilizationNode]) -> GapReport:
|
|
99
|
+
"""
|
|
100
|
+
Perform comprehensive gap analysis on a set of nodes.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
nodes: List of CivilizationNode instances to analyze
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
GapReport with all detected gaps and recommendations
|
|
107
|
+
"""
|
|
108
|
+
from uuid import uuid4
|
|
109
|
+
|
|
110
|
+
report = GapReport(org_id=self.org_id)
|
|
111
|
+
gaps: list[KnowledgeGap] = []
|
|
112
|
+
|
|
113
|
+
# Group nodes by type
|
|
114
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]] = {}
|
|
115
|
+
for node in nodes:
|
|
116
|
+
nodes_by_type.setdefault(node.node_type, []).append(node)
|
|
117
|
+
|
|
118
|
+
# Run all detection checks
|
|
119
|
+
gaps.extend(self._check_incomplete_nodes(nodes))
|
|
120
|
+
gaps.extend(self._check_orphan_nodes(nodes, nodes_by_type))
|
|
121
|
+
gaps.extend(self._check_stale_content(nodes))
|
|
122
|
+
gaps.extend(self._check_missing_owners(nodes))
|
|
123
|
+
gaps.extend(self._check_coverage_gaps(nodes_by_type))
|
|
124
|
+
gaps.extend(self._check_relationship_gaps(nodes, nodes_by_type))
|
|
125
|
+
|
|
126
|
+
report.gaps = gaps
|
|
127
|
+
report.coverage_score = self._calculate_coverage_score(nodes_by_type)
|
|
128
|
+
report.health_score = self._calculate_health_score(gaps, len(nodes))
|
|
129
|
+
report.recommendations = self._generate_recommendations(gaps, nodes_by_type)
|
|
130
|
+
|
|
131
|
+
return report
|
|
132
|
+
|
|
133
|
+
def _check_incomplete_nodes(self, nodes: list[CivilizationNode]) -> list[KnowledgeGap]:
|
|
134
|
+
"""Check for nodes with missing required fields."""
|
|
135
|
+
from uuid import uuid4
|
|
136
|
+
gaps = []
|
|
137
|
+
|
|
138
|
+
for node in nodes:
|
|
139
|
+
issues = []
|
|
140
|
+
|
|
141
|
+
if isinstance(node, SOP):
|
|
142
|
+
if len(node.steps) < self.MIN_SOP_STEPS:
|
|
143
|
+
issues.append("SOP has too few steps")
|
|
144
|
+
if not node.purpose:
|
|
145
|
+
issues.append("SOP missing purpose")
|
|
146
|
+
if not node.roles_involved:
|
|
147
|
+
issues.append("SOP missing roles")
|
|
148
|
+
|
|
149
|
+
elif isinstance(node, Role):
|
|
150
|
+
if len(node.responsibilities) < self.MIN_ROLE_RESPONSIBILITIES:
|
|
151
|
+
issues.append("Role has too few responsibilities")
|
|
152
|
+
if not node.accountabilities:
|
|
153
|
+
issues.append("Role missing accountabilities")
|
|
154
|
+
|
|
155
|
+
elif isinstance(node, Checklist):
|
|
156
|
+
if len(node.items) < self.MIN_CHECKLIST_ITEMS:
|
|
157
|
+
issues.append("Checklist has too few items")
|
|
158
|
+
|
|
159
|
+
elif isinstance(node, Runbook):
|
|
160
|
+
if not node.steps:
|
|
161
|
+
issues.append("Runbook has no steps")
|
|
162
|
+
if not node.escalation_contacts:
|
|
163
|
+
issues.append("Runbook missing escalation contacts")
|
|
164
|
+
|
|
165
|
+
if issues:
|
|
166
|
+
gaps.append(KnowledgeGap(
|
|
167
|
+
id=uuid4(),
|
|
168
|
+
gap_type=GapType.INCOMPLETE_NODE,
|
|
169
|
+
severity=GapSeverity.MEDIUM,
|
|
170
|
+
title=f"Incomplete {node.node_type.value}: {node.title}",
|
|
171
|
+
description="; ".join(issues),
|
|
172
|
+
affected_node_id=node.id,
|
|
173
|
+
affected_node_type=node.node_type,
|
|
174
|
+
suggested_action=f"Review and complete the {node.node_type.value}",
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
return gaps
|
|
178
|
+
|
|
179
|
+
def _check_orphan_nodes(
|
|
180
|
+
self,
|
|
181
|
+
nodes: list[CivilizationNode],
|
|
182
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]],
|
|
183
|
+
) -> list[KnowledgeGap]:
|
|
184
|
+
"""Check for nodes with no relationships."""
|
|
185
|
+
from uuid import uuid4
|
|
186
|
+
gaps = []
|
|
187
|
+
|
|
188
|
+
# SOPs without related SOPs or roles
|
|
189
|
+
for sop in nodes_by_type.get(NodeType.SOP, []):
|
|
190
|
+
if isinstance(sop, SOP):
|
|
191
|
+
if not sop.related_sops and not sop.roles_involved:
|
|
192
|
+
gaps.append(KnowledgeGap(
|
|
193
|
+
id=uuid4(),
|
|
194
|
+
gap_type=GapType.ORPHAN_NODE,
|
|
195
|
+
severity=GapSeverity.LOW,
|
|
196
|
+
title=f"Isolated SOP: {sop.title}",
|
|
197
|
+
description="SOP has no related SOPs or roles defined",
|
|
198
|
+
affected_node_id=sop.id,
|
|
199
|
+
affected_node_type=NodeType.SOP,
|
|
200
|
+
suggested_action="Link this SOP to related procedures and roles",
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
return gaps
|
|
204
|
+
|
|
205
|
+
def _check_stale_content(self, nodes: list[CivilizationNode]) -> list[KnowledgeGap]:
|
|
206
|
+
"""Check for outdated content."""
|
|
207
|
+
from uuid import uuid4
|
|
208
|
+
gaps = []
|
|
209
|
+
now = datetime.utcnow()
|
|
210
|
+
|
|
211
|
+
for node in nodes:
|
|
212
|
+
age_days = (now - node.updated_at).days
|
|
213
|
+
|
|
214
|
+
if age_days > self.STALE_DAYS_CRITICAL:
|
|
215
|
+
gaps.append(KnowledgeGap(
|
|
216
|
+
id=uuid4(),
|
|
217
|
+
gap_type=GapType.STALE_CONTENT,
|
|
218
|
+
severity=GapSeverity.HIGH,
|
|
219
|
+
title=f"Critically stale: {node.title}",
|
|
220
|
+
description=f"Not updated in {age_days} days",
|
|
221
|
+
affected_node_id=node.id,
|
|
222
|
+
affected_node_type=node.node_type,
|
|
223
|
+
suggested_action="Review and update this content urgently",
|
|
224
|
+
metadata={"days_since_update": age_days},
|
|
225
|
+
))
|
|
226
|
+
elif age_days > self.STALE_DAYS_WARNING:
|
|
227
|
+
gaps.append(KnowledgeGap(
|
|
228
|
+
id=uuid4(),
|
|
229
|
+
gap_type=GapType.STALE_CONTENT,
|
|
230
|
+
severity=GapSeverity.MEDIUM,
|
|
231
|
+
title=f"Stale content: {node.title}",
|
|
232
|
+
description=f"Not updated in {age_days} days",
|
|
233
|
+
affected_node_id=node.id,
|
|
234
|
+
affected_node_type=node.node_type,
|
|
235
|
+
suggested_action="Consider reviewing this content",
|
|
236
|
+
metadata={"days_since_update": age_days},
|
|
237
|
+
))
|
|
238
|
+
|
|
239
|
+
return gaps
|
|
240
|
+
|
|
241
|
+
def _check_missing_owners(self, nodes: list[CivilizationNode]) -> list[KnowledgeGap]:
|
|
242
|
+
"""Check for nodes without owners."""
|
|
243
|
+
from uuid import uuid4
|
|
244
|
+
gaps = []
|
|
245
|
+
|
|
246
|
+
for node in nodes:
|
|
247
|
+
if not node.owner_id:
|
|
248
|
+
# SOPs and Runbooks without owners are higher severity
|
|
249
|
+
severity = GapSeverity.HIGH if node.node_type in {NodeType.SOP, NodeType.RUNBOOK} else GapSeverity.LOW
|
|
250
|
+
gaps.append(KnowledgeGap(
|
|
251
|
+
id=uuid4(),
|
|
252
|
+
gap_type=GapType.MISSING_OWNER,
|
|
253
|
+
severity=severity,
|
|
254
|
+
title=f"No owner: {node.title}",
|
|
255
|
+
description=f"{node.node_type.value} has no assigned owner",
|
|
256
|
+
affected_node_id=node.id,
|
|
257
|
+
affected_node_type=node.node_type,
|
|
258
|
+
suggested_action="Assign an owner to this content",
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
return gaps
|
|
262
|
+
|
|
263
|
+
def _check_coverage_gaps(
|
|
264
|
+
self,
|
|
265
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]],
|
|
266
|
+
) -> list[KnowledgeGap]:
|
|
267
|
+
"""Check for missing essential node types."""
|
|
268
|
+
from uuid import uuid4
|
|
269
|
+
gaps = []
|
|
270
|
+
|
|
271
|
+
# Essential node types for a well-documented org
|
|
272
|
+
essential_types = {
|
|
273
|
+
NodeType.ORG_CHART: "Organization structure",
|
|
274
|
+
NodeType.ROLE: "Role definitions",
|
|
275
|
+
NodeType.SOP: "Standard operating procedures",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for node_type, description in essential_types.items():
|
|
279
|
+
if node_type not in nodes_by_type or len(nodes_by_type[node_type]) == 0:
|
|
280
|
+
gaps.append(KnowledgeGap(
|
|
281
|
+
id=uuid4(),
|
|
282
|
+
gap_type=GapType.COVERAGE_GAP,
|
|
283
|
+
severity=GapSeverity.HIGH,
|
|
284
|
+
title=f"Missing {description}",
|
|
285
|
+
description=f"No {node_type.value} nodes found in knowledge base",
|
|
286
|
+
suggested_action=f"Create {node_type.value} documentation",
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
return gaps
|
|
290
|
+
|
|
291
|
+
def _check_relationship_gaps(
|
|
292
|
+
self,
|
|
293
|
+
nodes: list[CivilizationNode],
|
|
294
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]],
|
|
295
|
+
) -> list[KnowledgeGap]:
|
|
296
|
+
"""Check for missing relationships between nodes."""
|
|
297
|
+
from uuid import uuid4
|
|
298
|
+
gaps = []
|
|
299
|
+
|
|
300
|
+
# Check if roles mentioned in SOPs exist
|
|
301
|
+
role_titles = {
|
|
302
|
+
r.role_title.lower() for r in nodes_by_type.get(NodeType.ROLE, [])
|
|
303
|
+
if isinstance(r, Role)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for sop in nodes_by_type.get(NodeType.SOP, []):
|
|
307
|
+
if isinstance(sop, SOP):
|
|
308
|
+
for role_name in sop.roles_involved:
|
|
309
|
+
if role_name.lower() not in role_titles:
|
|
310
|
+
gaps.append(KnowledgeGap(
|
|
311
|
+
id=uuid4(),
|
|
312
|
+
gap_type=GapType.MISSING_RELATIONSHIP,
|
|
313
|
+
severity=GapSeverity.MEDIUM,
|
|
314
|
+
title=f"Undefined role: {role_name}",
|
|
315
|
+
description=f"Role '{role_name}' referenced in SOP '{sop.title}' but not defined",
|
|
316
|
+
affected_node_id=sop.id,
|
|
317
|
+
affected_node_type=NodeType.SOP,
|
|
318
|
+
suggested_action=f"Create role definition for '{role_name}'",
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
return gaps
|
|
322
|
+
|
|
323
|
+
def _calculate_coverage_score(
|
|
324
|
+
self,
|
|
325
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]],
|
|
326
|
+
) -> float:
|
|
327
|
+
"""Calculate coverage score (0-100) based on node type diversity."""
|
|
328
|
+
essential_types = {NodeType.ORG_CHART, NodeType.ROLE, NodeType.SOP, NodeType.CHECKLIST}
|
|
329
|
+
covered = sum(1 for t in essential_types if t in nodes_by_type and nodes_by_type[t])
|
|
330
|
+
return (covered / len(essential_types)) * 100
|
|
331
|
+
|
|
332
|
+
def _calculate_health_score(self, gaps: list[KnowledgeGap], total_nodes: int) -> float:
|
|
333
|
+
"""Calculate health score (0-100) based on gap severity."""
|
|
334
|
+
if total_nodes == 0:
|
|
335
|
+
return 0.0
|
|
336
|
+
|
|
337
|
+
# Weight by severity
|
|
338
|
+
severity_weights = {
|
|
339
|
+
GapSeverity.CRITICAL: 4,
|
|
340
|
+
GapSeverity.HIGH: 2,
|
|
341
|
+
GapSeverity.MEDIUM: 1,
|
|
342
|
+
GapSeverity.LOW: 0.5,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
total_weight = sum(severity_weights[g.severity] for g in gaps)
|
|
346
|
+
# Higher weight = lower score
|
|
347
|
+
max_weight = total_nodes * 2 # Assume average 2 issues per node is worst case
|
|
348
|
+
score = max(0, 100 - (total_weight / max_weight) * 100)
|
|
349
|
+
return min(100, score)
|
|
350
|
+
|
|
351
|
+
def _generate_recommendations(
|
|
352
|
+
self,
|
|
353
|
+
gaps: list[KnowledgeGap],
|
|
354
|
+
nodes_by_type: dict[NodeType, list[CivilizationNode]],
|
|
355
|
+
) -> list[str]:
|
|
356
|
+
"""Generate prioritized recommendations."""
|
|
357
|
+
recommendations = []
|
|
358
|
+
|
|
359
|
+
# Critical issues first
|
|
360
|
+
critical = [g for g in gaps if g.severity == GapSeverity.CRITICAL]
|
|
361
|
+
if critical:
|
|
362
|
+
recommendations.append(f"Address {len(critical)} critical gaps immediately")
|
|
363
|
+
|
|
364
|
+
# Coverage gaps
|
|
365
|
+
coverage_gaps = [g for g in gaps if g.gap_type == GapType.COVERAGE_GAP]
|
|
366
|
+
if coverage_gaps:
|
|
367
|
+
types = ", ".join(g.title.replace("Missing ", "") for g in coverage_gaps)
|
|
368
|
+
recommendations.append(f"Document missing areas: {types}")
|
|
369
|
+
|
|
370
|
+
# Stale content
|
|
371
|
+
stale = [g for g in gaps if g.gap_type == GapType.STALE_CONTENT]
|
|
372
|
+
if stale:
|
|
373
|
+
recommendations.append(f"Review {len(stale)} pieces of stale content")
|
|
374
|
+
|
|
375
|
+
# Missing owners
|
|
376
|
+
unowned = [g for g in gaps if g.gap_type == GapType.MISSING_OWNER]
|
|
377
|
+
if unowned:
|
|
378
|
+
recommendations.append(f"Assign owners to {len(unowned)} unowned items")
|
|
379
|
+
|
|
380
|
+
return recommendations
|