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,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Process graph for representing workflows, SOPs, and procedural knowledge.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Iterator
|
|
9
|
+
from uuid import UUID, uuid4
|
|
10
|
+
|
|
11
|
+
from ..schema import SOP, SOPStep, Workflow, WorkflowNode, WorkflowEdge, Checklist
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProcessNodeType(str, Enum):
|
|
17
|
+
START = "start"
|
|
18
|
+
END = "end"
|
|
19
|
+
TASK = "task"
|
|
20
|
+
DECISION = "decision"
|
|
21
|
+
APPROVAL = "approval"
|
|
22
|
+
SUBPROCESS = "subprocess"
|
|
23
|
+
PARALLEL_GATEWAY = "parallel_gateway"
|
|
24
|
+
JOIN_GATEWAY = "join_gateway"
|
|
25
|
+
EVENT = "event"
|
|
26
|
+
WAIT = "wait"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProcessEdgeType(str, Enum):
|
|
30
|
+
SEQUENCE = "sequence"
|
|
31
|
+
CONDITIONAL = "conditional"
|
|
32
|
+
DEFAULT = "default"
|
|
33
|
+
EXCEPTION = "exception"
|
|
34
|
+
TIMEOUT = "timeout"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ProcessNode:
|
|
39
|
+
"""A node in the process graph."""
|
|
40
|
+
id: str
|
|
41
|
+
node_type: ProcessNodeType
|
|
42
|
+
label: str
|
|
43
|
+
description: str | None = None
|
|
44
|
+
responsible_role: str | None = None
|
|
45
|
+
tools: list[str] = field(default_factory=list)
|
|
46
|
+
duration_minutes: int | None = None
|
|
47
|
+
sla_minutes: int | None = None
|
|
48
|
+
metadata: dict = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
def __hash__(self):
|
|
51
|
+
return hash(self.id)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ProcessEdge:
|
|
56
|
+
"""An edge connecting two process nodes."""
|
|
57
|
+
source_id: str
|
|
58
|
+
target_id: str
|
|
59
|
+
edge_type: ProcessEdgeType = ProcessEdgeType.SEQUENCE
|
|
60
|
+
condition: str | None = None
|
|
61
|
+
probability: float | None = None # For decision edges
|
|
62
|
+
metadata: dict = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def __hash__(self):
|
|
65
|
+
return hash((self.source_id, self.target_id))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProcessGraph:
|
|
69
|
+
"""
|
|
70
|
+
Graph representation of a process or workflow.
|
|
71
|
+
Supports path analysis, critical path detection, and process validation.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, process_id: UUID, name: str):
|
|
75
|
+
self.process_id = process_id
|
|
76
|
+
self.name = name
|
|
77
|
+
self._nodes: dict[str, ProcessNode] = {}
|
|
78
|
+
self._edges: list[ProcessEdge] = []
|
|
79
|
+
self._adjacency: dict[str, list[ProcessEdge]] = {}
|
|
80
|
+
self._reverse_adjacency: dict[str, list[ProcessEdge]] = {}
|
|
81
|
+
|
|
82
|
+
def add_node(self, node: ProcessNode) -> None:
|
|
83
|
+
"""Add a node to the graph."""
|
|
84
|
+
self._nodes[node.id] = node
|
|
85
|
+
if node.id not in self._adjacency:
|
|
86
|
+
self._adjacency[node.id] = []
|
|
87
|
+
if node.id not in self._reverse_adjacency:
|
|
88
|
+
self._reverse_adjacency[node.id] = []
|
|
89
|
+
|
|
90
|
+
def add_edge(self, edge: ProcessEdge) -> None:
|
|
91
|
+
"""Add an edge to the graph."""
|
|
92
|
+
if edge.source_id not in self._nodes or edge.target_id not in self._nodes:
|
|
93
|
+
raise ValueError("Both source and target nodes must exist in the graph")
|
|
94
|
+
self._edges.append(edge)
|
|
95
|
+
self._adjacency[edge.source_id].append(edge)
|
|
96
|
+
self._reverse_adjacency[edge.target_id].append(edge)
|
|
97
|
+
|
|
98
|
+
def get_node(self, node_id: str) -> ProcessNode | None:
|
|
99
|
+
"""Get a node by ID."""
|
|
100
|
+
return self._nodes.get(node_id)
|
|
101
|
+
|
|
102
|
+
def get_start_nodes(self) -> list[ProcessNode]:
|
|
103
|
+
"""Get all start nodes."""
|
|
104
|
+
return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.START]
|
|
105
|
+
|
|
106
|
+
def get_end_nodes(self) -> list[ProcessNode]:
|
|
107
|
+
"""Get all end nodes."""
|
|
108
|
+
return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.END]
|
|
109
|
+
|
|
110
|
+
def get_successors(self, node_id: str) -> list[ProcessNode]:
|
|
111
|
+
"""Get all successor nodes."""
|
|
112
|
+
edges = self._adjacency.get(node_id, [])
|
|
113
|
+
return [self._nodes[e.target_id] for e in edges if e.target_id in self._nodes]
|
|
114
|
+
|
|
115
|
+
def get_predecessors(self, node_id: str) -> list[ProcessNode]:
|
|
116
|
+
"""Get all predecessor nodes."""
|
|
117
|
+
edges = self._reverse_adjacency.get(node_id, [])
|
|
118
|
+
return [self._nodes[e.source_id] for e in edges if e.source_id in self._nodes]
|
|
119
|
+
|
|
120
|
+
def get_decision_nodes(self) -> list[ProcessNode]:
|
|
121
|
+
"""Get all decision nodes."""
|
|
122
|
+
return [n for n in self._nodes.values() if n.node_type == ProcessNodeType.DECISION]
|
|
123
|
+
|
|
124
|
+
def find_all_paths(
|
|
125
|
+
self,
|
|
126
|
+
start_id: str | None = None,
|
|
127
|
+
end_id: str | None = None,
|
|
128
|
+
) -> list[list[str]]:
|
|
129
|
+
"""Find all paths from start to end nodes."""
|
|
130
|
+
starts = [start_id] if start_id else [n.id for n in self.get_start_nodes()]
|
|
131
|
+
ends = set([end_id]) if end_id else set(n.id for n in self.get_end_nodes())
|
|
132
|
+
|
|
133
|
+
all_paths = []
|
|
134
|
+
|
|
135
|
+
for start in starts:
|
|
136
|
+
stack = [(start, [start])]
|
|
137
|
+
while stack:
|
|
138
|
+
current, path = stack.pop()
|
|
139
|
+
if current in ends:
|
|
140
|
+
all_paths.append(path)
|
|
141
|
+
continue
|
|
142
|
+
for edge in self._adjacency.get(current, []):
|
|
143
|
+
if edge.target_id not in path: # Avoid cycles
|
|
144
|
+
stack.append((edge.target_id, path + [edge.target_id]))
|
|
145
|
+
|
|
146
|
+
return all_paths
|
|
147
|
+
|
|
148
|
+
def calculate_critical_path(self) -> list[ProcessNode]:
|
|
149
|
+
"""
|
|
150
|
+
Calculate the critical path (longest path through the process).
|
|
151
|
+
Uses node durations if available.
|
|
152
|
+
"""
|
|
153
|
+
all_paths = self.find_all_paths()
|
|
154
|
+
if not all_paths:
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
def path_duration(path: list[str]) -> int:
|
|
158
|
+
total = 0
|
|
159
|
+
for node_id in path:
|
|
160
|
+
node = self._nodes.get(node_id)
|
|
161
|
+
if node and node.duration_minutes:
|
|
162
|
+
total += node.duration_minutes
|
|
163
|
+
return total
|
|
164
|
+
|
|
165
|
+
critical = max(all_paths, key=path_duration)
|
|
166
|
+
return [self._nodes[node_id] for node_id in critical]
|
|
167
|
+
|
|
168
|
+
def estimate_total_duration(self) -> int:
|
|
169
|
+
"""Estimate total process duration based on critical path."""
|
|
170
|
+
critical = self.calculate_critical_path()
|
|
171
|
+
return sum(n.duration_minutes or 0 for n in critical)
|
|
172
|
+
|
|
173
|
+
def validate(self) -> list[str]:
|
|
174
|
+
"""
|
|
175
|
+
Validate the process graph structure.
|
|
176
|
+
Returns list of validation issues.
|
|
177
|
+
"""
|
|
178
|
+
issues = []
|
|
179
|
+
|
|
180
|
+
# Check for start node
|
|
181
|
+
starts = self.get_start_nodes()
|
|
182
|
+
if not starts:
|
|
183
|
+
issues.append("Process has no START node")
|
|
184
|
+
elif len(starts) > 1:
|
|
185
|
+
issues.append(f"Process has {len(starts)} START nodes (expected 1)")
|
|
186
|
+
|
|
187
|
+
# Check for end node
|
|
188
|
+
ends = self.get_end_nodes()
|
|
189
|
+
if not ends:
|
|
190
|
+
issues.append("Process has no END node")
|
|
191
|
+
|
|
192
|
+
# Check for unreachable nodes
|
|
193
|
+
if starts:
|
|
194
|
+
reachable = self._get_reachable_nodes(starts[0].id)
|
|
195
|
+
unreachable = set(self._nodes.keys()) - reachable
|
|
196
|
+
if unreachable:
|
|
197
|
+
issues.append(f"Unreachable nodes: {unreachable}")
|
|
198
|
+
|
|
199
|
+
# Check for dead ends (non-end nodes with no outgoing edges)
|
|
200
|
+
for node_id, node in self._nodes.items():
|
|
201
|
+
if node.node_type != ProcessNodeType.END:
|
|
202
|
+
if not self._adjacency.get(node_id):
|
|
203
|
+
issues.append(f"Dead end: {node_id} ({node.label})")
|
|
204
|
+
|
|
205
|
+
# Check for cycles
|
|
206
|
+
if self._has_cycle():
|
|
207
|
+
issues.append("Process contains a cycle")
|
|
208
|
+
|
|
209
|
+
return issues
|
|
210
|
+
|
|
211
|
+
def _get_reachable_nodes(self, start_id: str) -> set[str]:
|
|
212
|
+
"""Get all nodes reachable from a starting node."""
|
|
213
|
+
visited = set()
|
|
214
|
+
stack = [start_id]
|
|
215
|
+
|
|
216
|
+
while stack:
|
|
217
|
+
current = stack.pop()
|
|
218
|
+
if current in visited:
|
|
219
|
+
continue
|
|
220
|
+
visited.add(current)
|
|
221
|
+
for edge in self._adjacency.get(current, []):
|
|
222
|
+
stack.append(edge.target_id)
|
|
223
|
+
|
|
224
|
+
return visited
|
|
225
|
+
|
|
226
|
+
def _has_cycle(self) -> bool:
|
|
227
|
+
"""Check if graph has any cycles using DFS."""
|
|
228
|
+
visited = set()
|
|
229
|
+
rec_stack = set()
|
|
230
|
+
|
|
231
|
+
def dfs(node_id: str) -> bool:
|
|
232
|
+
visited.add(node_id)
|
|
233
|
+
rec_stack.add(node_id)
|
|
234
|
+
|
|
235
|
+
for edge in self._adjacency.get(node_id, []):
|
|
236
|
+
if edge.target_id not in visited:
|
|
237
|
+
if dfs(edge.target_id):
|
|
238
|
+
return True
|
|
239
|
+
elif edge.target_id in rec_stack:
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
rec_stack.remove(node_id)
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
for node_id in self._nodes:
|
|
246
|
+
if node_id not in visited:
|
|
247
|
+
if dfs(node_id):
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
def get_roles_involved(self) -> list[str]:
|
|
253
|
+
"""Get all roles involved in the process."""
|
|
254
|
+
roles = set()
|
|
255
|
+
for node in self._nodes.values():
|
|
256
|
+
if node.responsible_role:
|
|
257
|
+
roles.add(node.responsible_role)
|
|
258
|
+
return list(roles)
|
|
259
|
+
|
|
260
|
+
def get_tools_used(self) -> list[str]:
|
|
261
|
+
"""Get all tools used in the process."""
|
|
262
|
+
tools = set()
|
|
263
|
+
for node in self._nodes.values():
|
|
264
|
+
tools.update(node.tools)
|
|
265
|
+
return list(tools)
|
|
266
|
+
|
|
267
|
+
def get_node_by_role(self, role: str) -> list[ProcessNode]:
|
|
268
|
+
"""Get all nodes assigned to a specific role."""
|
|
269
|
+
return [n for n in self._nodes.values() if n.responsible_role == role]
|
|
270
|
+
|
|
271
|
+
def to_dict(self) -> dict:
|
|
272
|
+
"""Export graph to dictionary format."""
|
|
273
|
+
return {
|
|
274
|
+
"process_id": str(self.process_id),
|
|
275
|
+
"name": self.name,
|
|
276
|
+
"nodes": [
|
|
277
|
+
{
|
|
278
|
+
"id": n.id,
|
|
279
|
+
"node_type": n.node_type.value,
|
|
280
|
+
"label": n.label,
|
|
281
|
+
"description": n.description,
|
|
282
|
+
"responsible_role": n.responsible_role,
|
|
283
|
+
"tools": n.tools,
|
|
284
|
+
"duration_minutes": n.duration_minutes,
|
|
285
|
+
"metadata": n.metadata,
|
|
286
|
+
}
|
|
287
|
+
for n in self._nodes.values()
|
|
288
|
+
],
|
|
289
|
+
"edges": [
|
|
290
|
+
{
|
|
291
|
+
"source_id": e.source_id,
|
|
292
|
+
"target_id": e.target_id,
|
|
293
|
+
"edge_type": e.edge_type.value,
|
|
294
|
+
"condition": e.condition,
|
|
295
|
+
"metadata": e.metadata,
|
|
296
|
+
}
|
|
297
|
+
for e in self._edges
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def from_sop(cls, sop: SOP) -> "ProcessGraph":
|
|
303
|
+
"""Build a ProcessGraph from an SOP."""
|
|
304
|
+
graph = cls(process_id=sop.id, name=sop.title)
|
|
305
|
+
|
|
306
|
+
# Add start node
|
|
307
|
+
start = ProcessNode(
|
|
308
|
+
id="start",
|
|
309
|
+
node_type=ProcessNodeType.START,
|
|
310
|
+
label="Start",
|
|
311
|
+
description=sop.purpose,
|
|
312
|
+
)
|
|
313
|
+
graph.add_node(start)
|
|
314
|
+
|
|
315
|
+
# Add step nodes
|
|
316
|
+
prev_id = "start"
|
|
317
|
+
for step in sop.steps:
|
|
318
|
+
step_id = f"step_{step.step_number}"
|
|
319
|
+
node = ProcessNode(
|
|
320
|
+
id=step_id,
|
|
321
|
+
node_type=ProcessNodeType.TASK,
|
|
322
|
+
label=step.title,
|
|
323
|
+
description=step.description,
|
|
324
|
+
responsible_role=step.responsible_role,
|
|
325
|
+
tools=step.tools_required,
|
|
326
|
+
duration_minutes=step.estimated_duration_minutes,
|
|
327
|
+
)
|
|
328
|
+
graph.add_node(node)
|
|
329
|
+
graph.add_edge(ProcessEdge(source_id=prev_id, target_id=step_id))
|
|
330
|
+
prev_id = step_id
|
|
331
|
+
|
|
332
|
+
# Add end node
|
|
333
|
+
end = ProcessNode(
|
|
334
|
+
id="end",
|
|
335
|
+
node_type=ProcessNodeType.END,
|
|
336
|
+
label="End",
|
|
337
|
+
)
|
|
338
|
+
graph.add_node(end)
|
|
339
|
+
graph.add_edge(ProcessEdge(source_id=prev_id, target_id="end"))
|
|
340
|
+
|
|
341
|
+
return graph
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def from_workflow(cls, workflow: Workflow) -> "ProcessGraph":
|
|
345
|
+
"""Build a ProcessGraph from a Workflow node."""
|
|
346
|
+
graph = cls(process_id=workflow.id, name=workflow.title)
|
|
347
|
+
|
|
348
|
+
# Map workflow node types to process node types
|
|
349
|
+
type_map = {
|
|
350
|
+
"start": ProcessNodeType.START,
|
|
351
|
+
"end": ProcessNodeType.END,
|
|
352
|
+
"task": ProcessNodeType.TASK,
|
|
353
|
+
"decision": ProcessNodeType.DECISION,
|
|
354
|
+
"approval": ProcessNodeType.APPROVAL,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Add nodes
|
|
358
|
+
for wf_node in workflow.nodes:
|
|
359
|
+
node = ProcessNode(
|
|
360
|
+
id=wf_node.node_id,
|
|
361
|
+
node_type=type_map.get(wf_node.node_type, ProcessNodeType.TASK),
|
|
362
|
+
label=wf_node.label,
|
|
363
|
+
description=wf_node.description,
|
|
364
|
+
responsible_role=wf_node.responsible_role,
|
|
365
|
+
tools=[wf_node.tool] if wf_node.tool else [],
|
|
366
|
+
)
|
|
367
|
+
graph.add_node(node)
|
|
368
|
+
|
|
369
|
+
# Add edges
|
|
370
|
+
for wf_edge in workflow.edges:
|
|
371
|
+
edge_type = ProcessEdgeType.CONDITIONAL if wf_edge.condition else ProcessEdgeType.SEQUENCE
|
|
372
|
+
edge = ProcessEdge(
|
|
373
|
+
source_id=wf_edge.from_node,
|
|
374
|
+
target_id=wf_edge.to_node,
|
|
375
|
+
edge_type=edge_type,
|
|
376
|
+
condition=wf_edge.condition,
|
|
377
|
+
)
|
|
378
|
+
graph.add_edge(edge)
|
|
379
|
+
|
|
380
|
+
return graph
|
|
381
|
+
|
|
382
|
+
def __len__(self) -> int:
|
|
383
|
+
return len(self._nodes)
|
|
384
|
+
|
|
385
|
+
def __iter__(self) -> Iterator[ProcessNode]:
|
|
386
|
+
return iter(self._nodes.values())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ingestion module for Civilization Knowledge Layer.
|
|
3
|
+
Handles document parsing, chunking, extraction, and embedding.
|
|
4
|
+
"""
|
|
5
|
+
from .chunker import ProceduralChunker, SOPChunk, ChecklistChunk, RunbookChunk, GenericChunk
|
|
6
|
+
from .extractor import MetadataExtractor, EntityExtractor
|
|
7
|
+
from .embedder import CivilizationEmbedder
|
|
8
|
+
from .document_ingestor import DocumentIngestor
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ProceduralChunker",
|
|
12
|
+
"SOPChunk",
|
|
13
|
+
"ChecklistChunk",
|
|
14
|
+
"RunbookChunk",
|
|
15
|
+
"GenericChunk",
|
|
16
|
+
"MetadataExtractor",
|
|
17
|
+
"EntityExtractor",
|
|
18
|
+
"CivilizationEmbedder",
|
|
19
|
+
"DocumentIngestor",
|
|
20
|
+
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Procedural-aware chunker. Preserves step order and injects surrounding
|
|
3
|
+
context into each chunk so agents can retrieve step N without losing
|
|
4
|
+
the fact that step N-1 must happen first.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..schema import SOP, SOPStep, Checklist, ChecklistItem, Runbook, RunbookStep, CivilizationNode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SOPChunk:
|
|
16
|
+
sop_id: UUID
|
|
17
|
+
sop_title: str
|
|
18
|
+
step_number: int
|
|
19
|
+
step_title: str
|
|
20
|
+
content: str # Enriched with context
|
|
21
|
+
embedding: list[float] | None = None
|
|
22
|
+
metadata: dict = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ChecklistChunk:
|
|
27
|
+
checklist_id: UUID
|
|
28
|
+
checklist_title: str
|
|
29
|
+
item_number: int
|
|
30
|
+
description: str
|
|
31
|
+
content: str
|
|
32
|
+
embedding: list[float] | None = None
|
|
33
|
+
metadata: dict = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RunbookChunk:
|
|
38
|
+
runbook_id: UUID
|
|
39
|
+
runbook_title: str
|
|
40
|
+
step_number: int
|
|
41
|
+
action: str
|
|
42
|
+
content: str
|
|
43
|
+
embedding: list[float] | None = None
|
|
44
|
+
metadata: dict = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class GenericChunk:
|
|
49
|
+
node_id: UUID
|
|
50
|
+
node_title: str
|
|
51
|
+
chunk_index: int
|
|
52
|
+
content: str
|
|
53
|
+
embedding: list[float] | None = None
|
|
54
|
+
metadata: dict = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProceduralChunker:
|
|
58
|
+
"""
|
|
59
|
+
Chunks SOPs, checklists, and runbooks in a step-aware way.
|
|
60
|
+
Standard token-based chunking destroys SOPs by splitting across chunks.
|
|
61
|
+
Each chunk contains:
|
|
62
|
+
- The step/item itself
|
|
63
|
+
- SOP/checklist title and purpose (always present)
|
|
64
|
+
- Previous step summary (so agents know what happened before)
|
|
65
|
+
- Next step preview (so agents know what comes after)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, max_chunk_tokens: int = 512):
|
|
69
|
+
self.max_chunk_tokens = max_chunk_tokens
|
|
70
|
+
|
|
71
|
+
def chunk_sop(self, sop: SOP) -> list[SOPChunk]:
|
|
72
|
+
chunks = []
|
|
73
|
+
steps = sop.steps
|
|
74
|
+
for i, step in enumerate(steps):
|
|
75
|
+
prev_summary = ""
|
|
76
|
+
if i > 0:
|
|
77
|
+
prev = steps[i - 1]
|
|
78
|
+
prev_summary = f"Previous step ({prev.step_number}. {prev.title}): {prev.description[:120]}..."
|
|
79
|
+
|
|
80
|
+
next_preview = ""
|
|
81
|
+
if i < len(steps) - 1:
|
|
82
|
+
nxt = steps[i + 1]
|
|
83
|
+
next_preview = f"Next step ({nxt.step_number}. {nxt.title}): {nxt.description[:80]}..."
|
|
84
|
+
|
|
85
|
+
tools_str = ", ".join(step.tools_required) if step.tools_required else "none"
|
|
86
|
+
role_str = step.responsible_role or "unspecified"
|
|
87
|
+
|
|
88
|
+
content = (
|
|
89
|
+
f"SOP: {sop.title}\n"
|
|
90
|
+
f"Purpose: {sop.purpose}\n"
|
|
91
|
+
f"Scope: {sop.scope}\n"
|
|
92
|
+
f"---\n"
|
|
93
|
+
f"Step {step.step_number} of {len(steps)}: {step.title}\n"
|
|
94
|
+
f"Description: {step.description}\n"
|
|
95
|
+
f"Responsible role: {role_str}\n"
|
|
96
|
+
f"Tools required: {tools_str}\n"
|
|
97
|
+
)
|
|
98
|
+
if step.decision_points:
|
|
99
|
+
content += f"Decision points: {', '.join(step.decision_points)}\n"
|
|
100
|
+
if step.on_failure:
|
|
101
|
+
content += f"On failure: {step.on_failure}\n"
|
|
102
|
+
if step.estimated_duration_minutes:
|
|
103
|
+
content += f"Estimated duration: {step.estimated_duration_minutes} minutes\n"
|
|
104
|
+
if prev_summary:
|
|
105
|
+
content += f"---\n{prev_summary}\n"
|
|
106
|
+
if next_preview:
|
|
107
|
+
content += f"{next_preview}\n"
|
|
108
|
+
|
|
109
|
+
# Handle substeps
|
|
110
|
+
if step.substeps:
|
|
111
|
+
content += "Substeps:\n"
|
|
112
|
+
for substep in step.substeps:
|
|
113
|
+
content += f" {substep.step_number}. {substep.title}: {substep.description}\n"
|
|
114
|
+
|
|
115
|
+
chunks.append(SOPChunk(
|
|
116
|
+
sop_id=sop.id,
|
|
117
|
+
sop_title=sop.title,
|
|
118
|
+
step_number=step.step_number,
|
|
119
|
+
step_title=step.title,
|
|
120
|
+
content=content,
|
|
121
|
+
metadata={
|
|
122
|
+
"org_id": sop.org_id,
|
|
123
|
+
"tags": sop.tags,
|
|
124
|
+
"roles_involved": sop.roles_involved,
|
|
125
|
+
"tools_required": sop.tools_required,
|
|
126
|
+
}
|
|
127
|
+
))
|
|
128
|
+
return chunks
|
|
129
|
+
|
|
130
|
+
def chunk_checklist(self, checklist: Checklist) -> list[ChecklistChunk]:
|
|
131
|
+
chunks = []
|
|
132
|
+
items = checklist.items
|
|
133
|
+
for i, item in enumerate(items):
|
|
134
|
+
prev_summary = ""
|
|
135
|
+
if i > 0:
|
|
136
|
+
prev = items[i - 1]
|
|
137
|
+
prev_summary = f"Previous item ({prev.item_number}): {prev.description[:100]}..."
|
|
138
|
+
|
|
139
|
+
next_preview = ""
|
|
140
|
+
if i < len(items) - 1:
|
|
141
|
+
nxt = items[i + 1]
|
|
142
|
+
next_preview = f"Next item ({nxt.item_number}): {nxt.description[:80]}..."
|
|
143
|
+
|
|
144
|
+
content = (
|
|
145
|
+
f"Checklist: {checklist.title}\n"
|
|
146
|
+
f"Purpose: {checklist.purpose}\n"
|
|
147
|
+
f"---\n"
|
|
148
|
+
f"Item {item.item_number} of {len(items)}: {item.description}\n"
|
|
149
|
+
f"Required: {'Yes' if item.required else 'No'}\n"
|
|
150
|
+
)
|
|
151
|
+
if item.verification_method:
|
|
152
|
+
content += f"Verification: {item.verification_method}\n"
|
|
153
|
+
if item.responsible_role:
|
|
154
|
+
content += f"Responsible: {item.responsible_role}\n"
|
|
155
|
+
if item.time_estimate_minutes:
|
|
156
|
+
content += f"Time estimate: {item.time_estimate_minutes} minutes\n"
|
|
157
|
+
if item.notes:
|
|
158
|
+
content += f"Notes: {item.notes}\n"
|
|
159
|
+
if prev_summary:
|
|
160
|
+
content += f"---\n{prev_summary}\n"
|
|
161
|
+
if next_preview:
|
|
162
|
+
content += f"{next_preview}\n"
|
|
163
|
+
|
|
164
|
+
chunks.append(ChecklistChunk(
|
|
165
|
+
checklist_id=checklist.id,
|
|
166
|
+
checklist_title=checklist.title,
|
|
167
|
+
item_number=item.item_number,
|
|
168
|
+
description=item.description,
|
|
169
|
+
content=content,
|
|
170
|
+
metadata={
|
|
171
|
+
"org_id": checklist.org_id,
|
|
172
|
+
"tags": checklist.tags,
|
|
173
|
+
"frequency": checklist.frequency,
|
|
174
|
+
"trigger": checklist.trigger,
|
|
175
|
+
}
|
|
176
|
+
))
|
|
177
|
+
return chunks
|
|
178
|
+
|
|
179
|
+
def chunk_runbook(self, runbook: Runbook) -> list[RunbookChunk]:
|
|
180
|
+
chunks = []
|
|
181
|
+
steps = runbook.steps
|
|
182
|
+
for i, step in enumerate(steps):
|
|
183
|
+
prev_summary = ""
|
|
184
|
+
if i > 0:
|
|
185
|
+
prev = steps[i - 1]
|
|
186
|
+
prev_summary = f"Previous step ({prev.step_number}): {prev.action[:100]}..."
|
|
187
|
+
|
|
188
|
+
next_preview = ""
|
|
189
|
+
if i < len(steps) - 1:
|
|
190
|
+
nxt = steps[i + 1]
|
|
191
|
+
next_preview = f"Next step ({nxt.step_number}): {nxt.action[:80]}..."
|
|
192
|
+
|
|
193
|
+
content = (
|
|
194
|
+
f"Runbook: {runbook.title}\n"
|
|
195
|
+
f"System: {runbook.system}\n"
|
|
196
|
+
f"Scenario: {runbook.scenario}\n"
|
|
197
|
+
f"Severity: {runbook.severity or 'unspecified'}\n"
|
|
198
|
+
f"---\n"
|
|
199
|
+
f"Step {step.step_number} of {len(steps)}: {step.action}\n"
|
|
200
|
+
)
|
|
201
|
+
if step.command:
|
|
202
|
+
content += f"Command: {step.command}\n"
|
|
203
|
+
if step.expected_output:
|
|
204
|
+
content += f"Expected output: {step.expected_output}\n"
|
|
205
|
+
if step.on_success:
|
|
206
|
+
content += f"On success: {step.on_success}\n"
|
|
207
|
+
if step.on_failure:
|
|
208
|
+
content += f"On failure: {step.on_failure}\n"
|
|
209
|
+
if step.rollback:
|
|
210
|
+
content += f"Rollback: {step.rollback}\n"
|
|
211
|
+
if prev_summary:
|
|
212
|
+
content += f"---\n{prev_summary}\n"
|
|
213
|
+
if next_preview:
|
|
214
|
+
content += f"{next_preview}\n"
|
|
215
|
+
|
|
216
|
+
chunks.append(RunbookChunk(
|
|
217
|
+
runbook_id=runbook.id,
|
|
218
|
+
runbook_title=runbook.title,
|
|
219
|
+
step_number=step.step_number,
|
|
220
|
+
action=step.action,
|
|
221
|
+
content=content,
|
|
222
|
+
metadata={
|
|
223
|
+
"org_id": runbook.org_id,
|
|
224
|
+
"tags": runbook.tags,
|
|
225
|
+
"system": runbook.system,
|
|
226
|
+
"severity": runbook.severity,
|
|
227
|
+
}
|
|
228
|
+
))
|
|
229
|
+
return chunks
|
|
230
|
+
|
|
231
|
+
def chunk_generic(self, node: CivilizationNode, text: str,
|
|
232
|
+
overlap_tokens: int = 50) -> list[GenericChunk]:
|
|
233
|
+
"""
|
|
234
|
+
Token-based chunking for non-procedural content.
|
|
235
|
+
Uses simple word-based approximation (1 token ~ 0.75 words).
|
|
236
|
+
"""
|
|
237
|
+
words = text.split()
|
|
238
|
+
words_per_chunk = int(self.max_chunk_tokens * 0.75)
|
|
239
|
+
overlap_words = int(overlap_tokens * 0.75)
|
|
240
|
+
|
|
241
|
+
chunks = []
|
|
242
|
+
start = 0
|
|
243
|
+
chunk_index = 0
|
|
244
|
+
|
|
245
|
+
while start < len(words):
|
|
246
|
+
end = min(start + words_per_chunk, len(words))
|
|
247
|
+
chunk_text = " ".join(words[start:end])
|
|
248
|
+
|
|
249
|
+
# Add node context header
|
|
250
|
+
content = (
|
|
251
|
+
f"{node.node_type.value.upper()}: {node.title}\n"
|
|
252
|
+
f"---\n"
|
|
253
|
+
f"{chunk_text}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
chunks.append(GenericChunk(
|
|
257
|
+
node_id=node.id,
|
|
258
|
+
node_title=node.title,
|
|
259
|
+
chunk_index=chunk_index,
|
|
260
|
+
content=content,
|
|
261
|
+
metadata={
|
|
262
|
+
"org_id": node.org_id,
|
|
263
|
+
"node_type": node.node_type.value,
|
|
264
|
+
"tags": node.tags,
|
|
265
|
+
}
|
|
266
|
+
))
|
|
267
|
+
|
|
268
|
+
if end >= len(words):
|
|
269
|
+
break
|
|
270
|
+
start = end - overlap_words
|
|
271
|
+
chunk_index += 1
|
|
272
|
+
|
|
273
|
+
return chunks
|